Validation Monad
Accumulate validation errors with Valid and Invalid variants
The Validation monad represents either a success value or a collection of errors. Unlike Result which short-circuits on the first error, Validation accumulates all errors. This is essential for form validation where users need to see all validation problems at once.
Validation[E, T] has two variants:
Valid(value)- Contains a success value of typeTInvalid(errors)- Contains a list of errors of typeE
Creating & Checking
Create Validation values using Valid() and Invalid() constructors. Use is_valid() and is_invalid() to check which variant you have.
from better_py import Valid, Invalid
# Create values
valid = Valid(42)
single_error = Invalid("Error message")
multiple_errors = Invalid(["Error 1", "Error 2"])
# Check variants
valid.is_valid() # True
invalid.is_invalid() # TrueChecking variants is safe and doesn't require exception handling. Unlike Result, Invalid always contains a list of errors—even a single error is wrapped in a list.
Extracting Values
Getting Success Values
The unwrap method returns the success value if Valid, or raises an error if Invalid.
Valid(42).unwrap() # 42
Invalid(["error"]).unwrap() # Raises ValueErrorUse unwrap only when you're certain the value is Valid, or within a fold where both cases are handled.
Getting Error Lists
The unwrap_errors method returns the list of errors if Invalid, or raises an error if Valid.
Invalid(["error1", "error2"]).unwrap_errors() # ["error1", "error2"]
Valid(42).unwrap_errors() # Raises ValueErrorUnlike Result which has a single error value, Validation always has a list. This is what enables accumulation.
Transforming Success Values
The map method applies a function only to the Valid value, leaving Invalid unchanged. This is useful for transforming successful results while preserving errors.
Valid(5).map(lambda x: x * 2) # Valid(10)
Invalid(["bad"]).map(lambda x: x * 2) # Invalid(["bad"])When map is called on Invalid, it returns the Invalid unchanged. All errors are preserved for later reporting.
Transforming Errors
The map_errors method applies a function to the error list, leaving Valid unchanged. Use this to format, enrich, or normalize errors.
Invalid(["error"]).map_errors(lambda es: [f"! {e}" for e in es]) # Invalid(["! error"])
Valid(42).map_errors(lambda es: es) # Valid(42)Common use cases include adding timestamps, converting error types, or formatting errors for user display.
Chaining Operations
The flat_map method chains operations that return Validation. If any step returns Invalid, the rest of the chain is skipped.
from better_py import Valid, Invalid
def validate_positive(x: int):
return Valid(x) if x > 0 else Invalid(["Must be positive"])
Valid(5).flat_map(validate_positive) # Valid(5)
Invalid(["bad"]).flat_map(validate_positive) # Invalid(["bad"])
Valid(-1).flat_map(validate_positive) # Invalid(["Must be positive"])Use flat_map for sequential validations where later steps depend on earlier ones. Each step receives the previous Valid value, or short-circuits on the first Invalid.
Applying Wrapped Functions
The ap method applies a function wrapped in a Validation to a value wrapped in a Validation. This is Validation's key feature: when both sides are Invalid, it accumulates errors from both.
from better_py import Valid, Invalid
# Valid + Valid = Valid
add = Valid(lambda x: x + 1)
val = Valid(5)
add.ap(val) # Valid(6)
# Invalid + Valid = Invalid
Invalid(["e1"]).ap(Valid(5)) # Invalid(["e1"])
# Valid + Invalid = Invalid
Valid(lambda x: x + 1).ap(Invalid(["e2"])) # Invalid(["e2"])
# Invalid + Invalid = Invalid (accumulates both)
Invalid(["e1"]).ap(Invalid(["e2"])) # Invalid(["e1", "e2"])This enables parallel validation of multiple fields while collecting all errors together. This is perfect for forms where users should see all validation problems at once.
Handling Both Cases
The fold method transforms both Valid and Invalid into a common result type. You provide two functions—one for each case—and fold applies the appropriate one.
from better_py import Valid, Invalid
def describe(validation):
return validation.fold(
on_invalid=lambda errs: f"Errors: {errs}",
on_valid=lambda val: f"Value: {val}"
)
describe(Valid(42)) # "Value: 42"
describe(Invalid(["error"])) # "Errors: ['error']"Fold is the ultimate pattern match for Validation. It's the only operation that lets you handle both cases in one expression and return a non-Validation result.
Converting to Result
The to_result method converts Validation to Result. Valid becomes Ok, while Invalid becomes Error with the first error from the list.
Valid(42).to_result() # Ok(42)
Invalid(["error1", "error2"]).to_result() # Error("error1")Use this when integrating Validation with code that expects Result. Note that additional errors in Invalid are lost—only the first error is preserved.
Real-World Pattern: Form Validation
from better_py import Valid, Invalid
def validate_username(username: str):
"""Validate username and accumulate all errors."""
errors = []
if len(username) < 3:
errors.append("Username too short")
if len(username) > 20:
errors.append("Username too long")
if not username.isalnum():
errors.append("Username must be alphanumeric")
return Valid(username) if not errors else Invalid(errors)
def validate_email(email: str):
"""Validate email and accumulate all errors."""
errors = []
if "@" not in email:
errors.append("Email must contain @")
if "." not in email.split("@")[-1]:
errors.append("Email must have domain")
return Valid(email) if not errors else Invalid(errors)
def validate_form(data: dict):
"""Validate entire form, accumulating all errors."""
username_validation = validate_username(data.get("username", ""))
email_validation = validate_email(data.get("email", ""))
# Use ap to accumulate errors from both validations
make_dict = Valid(lambda u, e: {"username": u, "email": e})
return username_validation.ap(make_dict).ap(email_validation)
# All valid
validate_form({"username": "alice", "email": "alice@example.com"})
# Valid({"username": "alice", "email": "alice@example.com"})
# Both invalid - accumulates all errors
validate_form({"username": "a", "email": "test"})
# Invalid(["Username too short", "Email must contain @", "Email must have domain"])This pattern shows Validation's power: users see all validation errors at once, not just the first one. The ap method accumulates errors from parallel validations, while flat_map chains sequential validations.
When to Use
Use Validation when:
- You need to collect multiple errors
- Validating forms or data with multiple fields
- You want to show all errors at once
- Accumulating errors is more valuable than failing fast
Don't use Validation when:
- You only need one error (use
Resultinstead) - Errors are independent of each other
- You need optional values (use
Maybeinstead)
Comparison with Result
| Feature | Validation | Result |
|---|---|---|
| Error handling | Accumulates errors | Short-circuits on first error |
| Use case | Form validation, multi-field validation | Single error scenarios |
| Variants | Valid, Invalid | Ok, Error |
| Error type | List of errors | Single error |
Rule of thumb: Use Validation when users need to see all errors at once (like form validation). Use Result when the first error should stop processing.