better-py

Maybe Monad

Handle optional values safely with Some and Nothing variants

The Maybe monad represents optional values - either a value (Some) or no value (Nothing). It's a type-safe alternative to using None in Python.

Maybe[T] has two variants:

  • Some(value) - Contains a value of type T
  • Nothing - Represents absence of a value

Creating & Checking

Create Maybe values using Some() and Nothing() constructors. Use is_some() and is_nothing() to check which variant you have.

from better_py import Some, Nothing

# Create values
some_value = Some(42)
empty = Nothing()

# Check variants
some_value.is_some()     # True
some_value.is_nothing()  # False
empty.is_some()          # False
empty.is_nothing()       # True

You can also use the Maybe class methods for creating values:

from better_py import Maybe

Maybe.some(42)           # Some(42)
Maybe.nothing()          # Nothing
Maybe.from_value(42)     # Some(42)
Maybe.from_value(None)   # Nothing

Use some_none() to explicitly wrap None as a value instead of treating it as absence:

Maybe.some_none()        # Some(None) - None is the value

Extracting Values

The unwrap method returns the value for Some or raises an error for Nothing. Use unwrap_or and unwrap_or_else to provide defaults.

from better_py import Some, Nothing

Some(42).unwrap()              # 42
Nothing().unwrap()             # Raises ValueError

Some(42).unwrap_or(0)          # 42
Nothing().unwrap_or(0)         # 0

Some(42).unwrap_or_else(lambda: expensive_computation())  # 42
Nothing().unwrap_or_else(lambda: expensive_computation()) # Computation called

Use unwrap when you're certain a value exists. Use unwrap_or for simple defaults. Use unwrap_or_else for expensive default computation that should only run if needed.

Transforming Values

The map method applies a function only to Some, leaving Nothing unchanged. This is useful for transforming optional values while preserving absence.

from better_py import Some, Nothing

Some(5).map(lambda x: x * 2)      # Some(10)
Nothing().map(lambda x: x * 2)    # Nothing

# Chain maps
Some(5).map(lambda x: x * 2).map(lambda x: x + 1)  # Some(11)

When map is called on Nothing, it returns Nothing unchanged—this short-circuiting behavior is key to Maybe's utility in optional value handling.

Chaining Operations

The flat_map method chains operations that return Maybe. If any step returns Nothing, the rest of the chain is skipped—absence propagates automatically.

from better_py import Some, Nothing

def find_user(user_id: int):
    return Some({"id": user_id, "name": "Alice"}) if user_id == 1 else Nothing()

def get_address(user: dict):
    return Some(user.get("address"))

def get_zipcode(address: dict):
    return Maybe.from_value(address.get("zipcode") if address else None)

# Chain operations
result = (find_user(1)
    .flat_map(get_address)
    .flat_map(get_zipcode))  # Some("12345") or Nothing

# Nothing short-circuits
result = (find_user(99)
    .flat_map(get_address)
    .flat_map(get_zipcode))  # Nothing

This replaces nested if x is not None checks with a flat, readable pipeline. Each step receives the previous Some value, or short-circuits on the first Nothing.

The methods bind and and_then are aliases for flat_map—use whichever reads best in your code.

Combining Maybes

The zip method combines multiple Maybe values into a tuple. If any value is Nothing, zip returns Nothing immediately.

from better_py import Maybe, Some, Nothing

Maybe.zip(Some(1), Some(2), Some(3))          # Some((1, 2, 3))
Maybe.zip(Some(1), Nothing(), Some(3))        # Nothing

Use zip when you need all values to proceed—like validating multiple optional fields where any absence should result in no result.

The ap method applies a function wrapped in a Maybe to a value wrapped in a Maybe. Both must be Some for the operation to succeed.

from better_py import Some

add_one = Some(lambda x: x + 1)
value = Some(5)

add_one.ap(value)      # Some(6)

Nothing().ap(value)    # Nothing
add_one.ap(Nothing())  # Nothing

The lift2 and lift3 methods transform regular functions into functions that work with Maybe values.

from better_py import Maybe, Some

def add(x, y):
    return x + y

Maybe.lift2(add, Some(5), Some(10))    # Some(15)
Maybe.lift2(add, Some(5), Nothing())  # Nothing

Use these when you have existing functions that don't know about Maybe, and you want to use them with Maybe values without rewriting the functions.

Fallback Values

The or_else method returns this Maybe if it's Some, or another Maybe if this one is Nothing. This is useful for chaining fallback values.

from better_py import Some, Nothing

Some(5).or_else(Some(10))       # Some(5)
Nothing().or_else(Some(10))     # Some(10)

# Chain fallbacks
result = (get_from_cache(key)
    .or_else(get_from_database(key))
    .or_else(get_from_api(key)))

Use or_else when you want to try multiple sources in order, falling back to the next if the previous returns Nothing.

Pattern Matching

The fold method handles both cases and returns a non-Maybe result. You provide two functions—one for each case—and fold applies the appropriate one.

from better_py import Some, Nothing

def describe(maybe):
    return maybe.fold(
        on_some=lambda value: f"Has value: {value}",
        on_nothing=lambda: "No value"
    )

describe(Some(42))   # "Has value: 42"
describe(Nothing())  # "No value"

fold is the ultimate pattern match for Maybe—it's the only operation that lets you handle both cases in one expression and return a non-Maybe result.

Real-World Pattern: Safe Dictionary Access

from better_py import Some, Nothing

users = {
    1: {"name": "Alice", "email": "alice@example.com"},
    2: {"name": "Bob"}
}

def get_user(users: dict, user_id: int):
    return Maybe.from_value(users.get(user_id))

def get_email(user: dict):
    return Maybe.from_value(user.get("email"))

# Safe nested access
email = (get_user(users, 1)
    .flat_map(get_email))  # Some("alice@example.com")

# Missing user returns Nothing
email = (get_user(users, 99)
    .flat_map(get_email))  # Nothing

# User exists but email is missing
email = (get_user(users, 2)
    .flat_map(get_email))  # Nothing

This pattern shows Maybe's strength: deeply nested optional access becomes a flat pipeline. If any step returns Nothing, the entire result is Nothing—no nested if checks or None coalescing needed.

Real-World Pattern: Validation Pipeline

from better_py import Some, Nothing

def validate_positive(x: int):
    return Some(x) if x > 0 else Nothing()

def validate_non_zero(x: int):
    return Some(x) if x != 0 else Nothing()

def safe_divide(a: int, b: int):
    return (validate_positive(a)
        .flat_map(lambda _: validate_non_zero(b))
        .map(lambda _: a / b))

safe_divide(10, 2)   # Some(5.0)
safe_divide(10, 0)   # Nothing
safe_divide(-5, 2)   # Nothing

This pattern shows Maybe's use in validation: each validation step can return Nothing if it fails, and the entire pipeline short-circuits. No intermediate variables or nested conditionals needed.

When to Use

Use Maybe when:

  • A value might be missing
  • You want to avoid None checks
  • You want type-safe optional handling
  • Absence of a value is normal behavior

Don't use Maybe when:

  • You need error messages (use Result instead)
  • You need to accumulate errors (use Validation instead)
  • The value should never be missing (use plain types)

Comparison with Result and Either

All three monads handle operations that might not produce a value, but they serve different purposes:

FeatureMaybeResultEither
PurposeOptional valuesError handlingTwo distinct possibilities
Generic typesMaybe[T] (value/none)Result[T, E] (success/error)Either[L, R] (any two types)
VariantsSome(value), NothingOk(value), Error(error)Left(value), Right(value)
Error context❌ None✅ Error type only✅ Any type
Use caseValues that might be missingOperations that failTwo-value alternatives

Examples:

# Maybe: Value exists or not
Maybe[User]  # Some(User) or Nothing

# Result: Success or error with context
Result[User, str]  # Ok(User) or Error("User not found")

# Either: Two possibilities with rich context
Either[APIError, User]  # Left(APIError) or Right(User)

Rule of thumb:

  • Use Maybe when a value might simply not exist (no error involved)
  • Use Result when you have explicit success/error cases
  • Use Either when Left/right are two distinct possibilities (beyond success/error)

See Also

  • Result - For error handling with messages
  • Validation - For accumulating errors
  • Either - For two-value alternatives

On this page