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 typeTNothing- 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() # TrueYou 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) # NothingUse some_none() to explicitly wrap None as a value instead of treating it as absence:
Maybe.some_none() # Some(None) - None is the valueExtracting 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 calledUse 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)) # NothingThis 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)) # NothingUse 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()) # NothingThe 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()) # NothingUse 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)) # NothingThis 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) # NothingThis 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
Nonechecks - You want type-safe optional handling
- Absence of a value is normal behavior
Don't use Maybe when:
- You need error messages (use
Resultinstead) - You need to accumulate errors (use
Validationinstead) - 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:
| Feature | Maybe | Result | Either |
|---|---|---|---|
| Purpose | Optional values | Error handling | Two distinct possibilities |
| Generic types | Maybe[T] (value/none) | Result[T, E] (success/error) | Either[L, R] (any two types) |
| Variants | Some(value), Nothing | Ok(value), Error(error) | Left(value), Right(value) |
| Error context | ❌ None | ✅ Error type only | ✅ Any type |
| Use case | Values that might be missing | Operations that fail | Two-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