better-py

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 type L (typically error)
  • Right(value) - Contains a value of type R (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()  # True

Checking 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:

FeatureEitherResultMaybe
PurposeTwo distinct possibilitiesError handlingOptional values
Generic typesEither[L, R] (any two types)Result[T, E] (success/error)Maybe[T] (value/none)
VariantsLeft(value), Right(value)Ok(value), Error(error)Some(value), Nothing
Error context✅ Any type✅ Error type only❌ None
ConventionGeneric (flexible)Success/error specificPresence/absence
Use caseTwo-value alternativesOperations that failValues 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 Nothing

Rule 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

On this page