Skip to content
Pyrula
Pre-release: the packages are not on PyPI yet, so pip install does not resolve. Build from source to try it today.

Validated

Validated[T, E] is Valid(value) or Invalid(errors). Use it when a form, config file or batch should report every error in one pass.

from pyrula import Validated, Valid, Invalid

zip combines two results. Two Valid give Valid((a, b)); any Invalid gives an Invalid with all errors concatenated.

def check_name(name: str) -> Validated[str, str]:
return Validated.valid(name) if name.strip() else Validated.invalid("name cannot be blank")
def check_age(age: int) -> Validated[int, str]:
return Validated.valid(age) if 0 <= age <= 130 else Validated.invalid("age must be between 0 and 130")
result = check_name("").zip(check_age(200))
# Invalid(['name cannot be blank', 'age must be between 0 and 130'])

traverse(values, fn) runs fn on every input, including those after an Invalid. sequence(values) collects existing results. Errors are flattened in encounter order, and success holds a list.

Validated.traverse(["Ada", "Lin"], check_name) # Valid(['Ada', 'Lin'])
Validated.traverse(["", "Ada", ""], check_name)
# Invalid(['name cannot be blank', 'name cannot be blank'])
Validated.sequence([Invalid(["name", "email"]), Valid(2), Invalid("age")])
# Invalid(['name', 'email', 'age'])

Callbacks and elements must be Validated, or TypeError is raised. Iterator and callback exceptions propagate and are not collected as errors. For stop-at-first-error, use Either.traverse.

CallResult
Validated.valid(v) / Valid(v)Valid(v)
Validated.invalid(e)Invalid([e])
Validated.invalid([e1, e2])Invalid([e1, e2])
Validated.cond(test, value, error)Valid(value) if test else Invalid([error])
Validated.valid(2).map(lambda x: x * 10) # Valid(20)
Invalid("too short").map_err(lambda errs: [e.upper() for e in errs])
# Invalid(['TOO SHORT'])
result.fold(
lambda errs: f"rejected: {', '.join(errs)}",
lambda val: f"accepted: {val}",
)

get_or_else takes an eager default.

match Validated.sequence([check_name(""), check_name("Ada")]):
case Valid(names):
message = f"accepted: {names}"
case Invalid(errors):
message = f"rejected: {', '.join(errors)}"
assert message == "rejected: name cannot be blank"

Invalid.errors is an immutable tuple, so an Invalid is hashable when its errors are. The constructor takes a list or tuple as the whole error collection and wraps anything else as one error. repr and the JSON form write the errors as a list.

Invalid("a").errors # ('a',)
Invalid(("a", "b")).errors # ('a', 'b'): two errors, not one tuple error
Invalid((("code", "msg"),)) # one error that is itself a tuple
from pyrula import Ok, Err
Validated.from_either(Err("e")) # Invalid(['e'])
Invalid("e").to_either() # Err(('e',)); the full error tuple is the payload

@do and @do_async do not accept Validated. Use zip, sequence or traverse to accumulate, and Either with @do for dependent steps. The JSON form is in JSON form.

View this page as Markdown