Validated
Either short-circuits on the first error; Validated accumulates all of them.
When you validate a form, parse a config file, or check a batch of inputs, you
usually want to tell the user about every problem at once, not force them to fix
one error, resubmit, hit another, and repeat. That is what Validated is for.
from pyrula import Validated, Valid, InvalidAccumulating errors with zip
Section titled “Accumulating errors with zip”Each check returns a Valid or an Invalid. zip combines two:
Valid + ValidyieldsValid((a, b)).Valid + InvalidorInvalid + ValidyieldsInvalidwith that side’s errors.Invalid + InvalidyieldsInvalidwith both error lists concatenated.
def check_name(name: str) -> Validated: return ( Validated.valid(name) if name.strip() else Validated.invalid("name cannot be blank") )
def check_age(age: int) -> Validated: return ( Validated.valid(age) if 0 <= age <= 130 else Validated.invalid("age must be between 0 and 130") )
def check_email(email: str) -> Validated: return ( Validated.valid(email) if "@" in email else Validated.invalid("email must contain @") )
result = check_name("").zip(check_age(200)).zip(check_email("notanemail"))# Invalid(['name cannot be blank', 'age must be between 0 and 130', 'email must contain @'])Every error surfaces in one pass.
Constructors
Section titled “Constructors”| Call | Result |
|---|---|
Validated.valid(v) / Valid(v) | Valid(v) |
Validated.invalid(e) | Invalid([e]) (wraps single error in a list) |
Validated.invalid([e1, e2]) | Invalid([e1, e2]) (list stored as-is, shallow copy) |
Validated.cond(test, value, error) | Valid(value) if test else Invalid([error]) |
Transform and fold
Section titled “Transform and fold”map runs on Valid and skips Invalid. map_err runs on Invalid’s error list.
Validated.valid(2).map(lambda x: x * 10) # Valid(20)Validated.invalid("e").map(lambda x: x * 10) # Invalid(['e']), unchanged
Invalid("too short").map_err(lambda errs: [e.upper() for e in errs])# Invalid(['TOO SHORT'])fold handles both branches at the edge of a system:
result.fold( lambda errs: f"rejected: {', '.join(errs)}", lambda val: f"accepted: {val}",)get_or_else returns the value (if Valid) or the default (if Invalid).
Bridge to Either
Section titled “Bridge to Either”from pyrula import Ok, Err
Validated.from_either(Ok(1)) # Valid(1)Validated.from_either(Err("e")) # Invalid(['e'])
Valid(1).to_either() # Ok(1)Invalid("e").to_either() # Err(['e']) (errors become the Err payload)to_either wraps the full error list as the Err value, so the caller sees
every accumulated error, not just the first.
Typing
Section titled “Typing”Validated[T, E] is a real generic. Valid and Invalid are subtypes:
def validate_port(raw: str) -> Validated[int, str]: if not raw.isdigit(): return Validated.invalid(f"not a number: {raw!r}") port = int(raw) return Validated.valid(port) if 0 < port < 65536 else Validated.invalid("out of range")isinstance(x, Validated) is true for both cases.
Scala equivalent
Section titled “Scala equivalent”cats.data.Validated / ValidatedNel[E, A] in Cats. ValidatedNel accumulates
errors in a NonEmptyList; pyrula uses a plain list (no non-empty enforcement at
the type level). The methods correspond: map, map_err (leftMap), fold,
toEither. The zip operator matches Cats’ |+| / product for accumulating
applicatives.
See also: Either for the short-circuiting alternative.