Option¶
What is Option?¶
Option[A] represents a value that may or may not be present. It is either Some(value) — containing a value of type A — or Nothing, representing the absence of a value.
from pyfect import option
present = option.some(42) # Some(value=42)
absent = option.nothing() # Nothing()
This is a structured alternative to using None directly. The key difference is that Option[A] is explicit in the type signature — a function returning Option[str] clearly communicates that the value may be absent, whereas str | None relies on the caller remembering to check.
Tony Hoare, who invented the null reference, famously called it his "Billion Dollar Mistake" — null references are so easy to forget to handle that they have caused incalculable bugs and crashes over the decades. Option is the structured answer to that problem.
Creating Options¶
From a known value or absence¶
nothing() always returns the same singleton NOTHING — no allocation on repeated calls.
From a nullable value¶
from_optional converts Python's A | None into an Option[A]:
from pyfect import option
option.from_optional("hello") # Some(value='hello')
option.from_optional(None) # Nothing()
option.from_optional(0) # Some(value=0) — falsy values are preserved
option.from_optional("") # Some(value='') — empty string is not None
Note that from_optional checks for None specifically — not falsiness. 0, "", and [] all become Some.
From a predicate¶
lift_predicate turns a predicate function into a constructor:
from pyfect import option
parse_positive = option.lift_predicate(lambda n: n > 0)
parse_positive(42) # Some(value=42)
parse_positive(-1) # Nothing()
Guards¶
Use is_some and is_nothing to check which variant you have. Both are proper type narrowing functions — after the check, the type checker knows exactly what you're working with:
from pyfect import option
opt = option.some(42)
if option.is_some(opt):
print(opt.value) # type checker knows opt is Some[int] here
Or use Python's native pattern matching directly:
from pyfect.option import Some, Nothing
match opt:
case Some(value):
print(f"Got: {value}")
case Nothing():
print("Nothing here")
Transforming Options¶
map¶
Transform the value inside a Some. Nothing passes through unchanged:
from pyfect import option, pipe
pipe(option.some(21), option.map(lambda x: x * 2)) # Some(value=42)
pipe(option.nothing(), option.map(lambda x: x * 2)) # Nothing()
flat_map¶
Chain a computation that itself may return Nothing:
from pyfect import option, pipe
def find_config(key: str) -> option.Option[str]:
config = {"host": "localhost"}
return option.from_optional(config.get(key))
def parse_port(s: str) -> option.Option[int]:
try:
return option.some(int(s))
except ValueError:
return option.nothing()
result = pipe(
find_config("port"),
option.flat_map(parse_port),
)
# Nothing() — "port" key not found
filter¶
Keep the value only if it satisfies a predicate:
from pyfect import option, pipe
pipe(option.some(42), option.filter(lambda x: x > 0)) # Some(value=42)
pipe(option.some(-1), option.filter(lambda x: x > 0)) # Nothing()
Extracting values¶
get_or_else¶
Provide a default value when Nothing:
from pyfect import option, pipe
pipe(option.some(42), option.get_or_else(lambda: 0)) # 42
pipe(option.nothing(), option.get_or_else(lambda: 0)) # 0
The default is a thunk — it is only evaluated when the option is Nothing.
get_or_none¶
Convert back to Python's A | None:
from pyfect import option, pipe
pipe(option.some(42), option.get_or_none) # 42
pipe(option.nothing(), option.get_or_none) # None
get_or_raise¶
Raise a ValueError if Nothing:
from pyfect import option, pipe
pipe(option.some(42), option.get_or_raise) # 42
pipe(option.nothing(), option.get_or_raise) # raises ValueError
Fallback¶
or_else¶
Try an alternative when Nothing:
from pyfect import option, pipe
pipe(option.some(42), option.or_else(lambda: option.some(0))) # Some(value=42)
pipe(option.nothing(), option.or_else(lambda: option.some(0))) # Some(value=0)
first_some_of¶
Return the first Some from an iterable:
from pyfect import option
option.first_some_of([option.nothing(), option.some(2), option.some(3)]) # Some(value=2)
option.first_some_of([option.nothing(), option.nothing()]) # Nothing()
Combining Options¶
zip_with¶
Combine two Option values with a function. If either is Nothing, the result is Nothing:
from pyfect import option
option.zip_with(option.some("Alice"), option.some(30), lambda name, age: f"{name} is {age}")
# Some(value='Alice is 30')
option.zip_with(option.some("Alice"), option.nothing(), lambda name, age: f"{name} is {age}")
# Nothing()
all¶
Combine a list or dict of Options. Returns Nothing if any element is Nothing:
from pyfect import option
option.all([option.some(1), option.some(2), option.some(3)]) # Some(value=[1, 2, 3])
option.all([option.some(1), option.nothing(), option.some(3)]) # Nothing()
option.all({"a": option.some(1), "b": option.some(2)}) # Some(value={'a': 1, 'b': 2})
Heterogeneous collections
Heterogeneous collections work at runtime, but Python's type system cannot unify different value types into a single Option[A]. Silence the type checker with # type: ignore and annotate the result explicitly:
Interop with Effect¶
Use effect.from_option to convert an Option into an Effect:
from pyfect import effect, option, pipe
def find_user(user_id: int) -> option.Option[str]:
users = {1: "Alice", 2: "Bob"}
return option.from_optional(users.get(user_id))
result = pipe(
find_user(99),
effect.from_option(lambda: "User 99 not found"),
)
match effect.run_sync_exit(result):
case effect.Success(value):
print(value)
case effect.Failure(error):
print(error) # User 99 not found
Some(value) becomes a successful effect. Nothing becomes a failed effect using the provided error thunk.