Either Monad
Handle two-value alternatives with Left and Right variants
The Either monad represents a value that can be one of two possibilities. Conventionally, Left represents an error and Right represents a success. Unlike exceptions, Either makes error paths explicit in your type signatures—errors cannot be ignored or accidentally propagated.
Either[L, R] has two variants:
Left(value)- Contains a value of typeL(typically error)Right(value)- Contains a value of typeR(typically success)
Creating & Checking
Create Either values using Left() and Right() constructors. Use is_left() and is_right() to check which variant you have.
from better_py import Left, Right
# Create values
error = Left("Something went wrong")
success = Right(42)
# Check variants
error.is_left() # True
error.is_right() # False
success.is_left() # False
success.is_right() # TrueChecking variants is safe and doesn't require exception handling. Use these methods when you need to branch logic based on success or error.
Transforming Right Values
The map method applies a function only to the Right value, leaving Left unchanged. This is useful for transforming successful results while preserving errors.
Right(5).map(lambda x: x * 2) # Right(10)
Left("error").map(lambda x: x * 2) # Left("error")When map is called on Left, it returns the Left value unchanged—this short-circuiting behavior is key to Either's utility in error handling.
Transforming Left Values
The map_left method applies a function only to the Left value, leaving Right unchanged. Use this to transform, enrich, or normalize error values.
Left("error").map_left(str.upper) # Left("ERROR")
Right(42).map_left(str.upper) # Right(42)Common use cases include adding timestamps to errors, converting error types, or formatting error messages for display.
Swapping Left and Right
The swap method exchanges Left and Right. This is useful when you want to reverse your error/success convention or when working with APIs that have different conventions.
Left(1).swap() # Right(1)
Right(2).swap() # Left(2)Swapping can also be used to convert between Either and other monads with different conventions.
Chaining Operations
The flat_map method chains operations that return Either. If any step returns Left, the rest of the chain is skipped—errors propagate automatically.
from better_py import Left, Right
def validate_positive(x: int):
return Right(x) if x > 0 else Left("Must be positive")
def validate_non_zero(x: int):
return Right(x) if x != 0 else Left("Must be non-zero")
def divide(a: int, b: int):
return (validate_positive(a)
.flat_map(lambda _: validate_non_zero(b))
.map(lambda _: a / b))
divide(10, 2) # Right(5.0)
divide(10, 0) # Left("Must be non-zero")
divide(-5, 2) # Left("Must be positive")This replaces nested if/else chains with a flat, readable pipeline. Each step receives the previous Right value, or short-circuits on the first Left.
Combining Multiple Eithers
The zip method combines multiple Either values into a tuple. If any value is Left, zip returns that Left immediately.
from better_py import Either, Left, Right
Either.zip(Right(1), Right(2), Right(3)) # Right((1, 2, 3))
Either.zip(Right(1), Left("bad"), Right(3)) # Left("bad")Use zip when you need all values to proceed—like validating multiple form fields where any failure should reject the entire form.
Applying Wrapped Functions
The ap method applies a function wrapped in an Either to a value wrapped in an Either. Both must be Right for the operation to succeed.
from better_py import Right
add = Right(lambda x: x + 1)
value = Right(5)
value.ap(add) # Right(6)
Left("bad").ap(add) # Left("bad")
value.ap(Left("bad")) # Left("bad")This enables function composition within the Either context. The receiver contains the value, while the argument contains the function—the reverse of what you might expect.
Lifting Regular Functions
The lift2 and lift3 methods transform regular functions into functions that work with Either values.
from better_py import Either, Right
def add(x, y):
return x + y
Either.lift2(add, Right(5), Right(10)) # Right(15)
Either.lift2(add, Right(5), Left("bad")) # Left("bad")Use these when you have existing functions that don't know about Either, and you want to use them with Either values without rewriting the functions.
Handling Both Cases
The fold method transforms both Left and Right into a common result type. You provide two functions—one for each case—and fold applies the appropriate one.
from better_py import Left, Right
def describe(either):
return either.fold(
on_left=lambda err: f"Error: {err}",
on_right=lambda val: f"Value: {val}"
)
describe(Left("error")) # "Error: error"
describe(Right(42)) # "Value: 42"Fold is the ultimate pattern match for Either—it's the only operation that lets you handle both cases in one expression and return a non-Either result.
Real-World Pattern: API Response Handling
from better_py import Left, Right
class APIError:
def __init__(self, message: str, status_code: int):
self.message = message
self.status_code = status_code
def fetch_user(user_id: int):
"""Fetch user from API. Returns Either[APIError, dict]."""
if user_id == 1:
return Right({"id": 1, "name": "Alice", "email": "alice@example.com"})
return Left(APIError("User not found", 404))
def validate_email(user: dict):
"""Validate email format. Returns Either[APIError, dict]."""
if "@" not in user.get("email", ""):
return Left(APIError("Invalid email", 400))
return Right(user)
def format_user(user: dict):
"""Format user for display."""
return f"{user['name']} ({user['email']})"
# Chain operations
result = (fetch_user(1)
.flat_map(validate_email)
.map(format_user))
# Right("Alice (alice@example.com)")
# With invalid user
result = (fetch_user(99)
.flat_map(validate_email)
.map(format_user))
# Left(APIError("User not found", 404))
# Handle both cases
display = result.fold(
on_left=lambda err: f"Error {err.status_code}: {err.message}",
on_right=lambda formatted: formatted
)This pattern shows the complete Either workflow: fetch, validate, transform, and finally fold to produce a user-facing message. Errors propagate automatically through each step.
When to Use
Use Either when:
- You need error values with rich context (custom error types)
- Left/right distinction is meaningful beyond success/error
- You want explicit error handling in your types
Don't use Either when:
- You only need optional values (use
Maybe) - You need simple error strings (use
Result) - You need to accumulate multiple errors (use
Validation)
Comparison with Result and Maybe
All three monads handle operations that might not produce a value, but they serve different purposes:
| Feature | Either | Result | Maybe |
|---|---|---|---|
| Purpose | Two distinct possibilities | Error handling | Optional values |
| Generic types | Either[L, R] (any two types) | Result[T, E] (success/error) | Maybe[T] (value/none) |
| Variants | Left(value), Right(value) | Ok(value), Error(error) | Some(value), Nothing |
| Error context | ✅ Any type | ✅ Error type only | ❌ None |
| Convention | Generic (flexible) | Success/error specific | Presence/absence |
| Use case | Two-value alternatives | Operations that fail | Values that might be missing |
Examples:
# Either: Two possibilities with rich error context
Either[APIError, User] # Left(APIError) or Right(User)
# Result: Success or error with context
Result[User, APIError] # Ok(User) or Error(APIError("User not found", 404))
# Maybe: Value exists or not
Maybe[User] # Some(User) or NothingRule of thumb:
- Use Either when Left/right are two distinct possibilities (beyond success/error)
- Use Result when you have explicit success/error cases
- Use Maybe when a value might simply not exist (no error involved)
See Also
- Result - For explicit error handling
- Maybe - For optional values
- Validation - For accumulating errors