Beyond ?.: Why Python Should Consider Monadic Composition
Diving into monadic composition as a powerful solution to gracefully handle 'None' in Python.
Handling None
gracefully shouldn't require mental gymnastics or syntactic clutter.
The Problem: Python and the Pain of None
If you've written Python for any length of time, you've probably danced around None
more than you care to admit.
if user and user.profile and user.profile.avatar:
url = user.profile.avatar.url
else:
url = None
This pattern is everywhere. It’s verbose, repetitive, and error-prone. Most modern languages have introduced syntactic relief in the form of optional chaining (?.
) and null-coalescing (??
) operators. Python almost followed suit.
PEP 505: A Practical, Rejected Solution
PEP 505 proposed exactly this: adding ?.
, ?[]
, and ??
to let developers write succinct, safe access chains:
url = user?.profile?.avatar?.url ?? default_url
On paper, it solves the problem elegantly. But Python’s core philosophy “there should be one obvious way to do it” clashes with adding more syntax for one-off problems.
PEP 505 ultimately clashes with Python’s preference for clarity and incremental growth over feature creep.
So, the proposal was rejected, yet the pain remained. However, while many (?most?) languages choose special operators to solve this problem and even Python has in limited use cases such as the walrus operator, special operators are not the only way to solve this pain point.
What If We Went Deeper?
PEP 505 tried to address a specific pain point with specific syntax. But in doing so, it ignored a broader, more powerful abstraction that could generalize not only optional values, but a whole class of context-aware computations.
Let’s talk about monadic composition.
Monadic Composition: The Generalized Solution
Monads get a bad rap in Python circles. Monads are often treated as academic, abstract, or “too functional” for imperative audiences, however at their core, monads are a pattern for composing operations, especially for composition with extra context: failure, logging, side effects, or asynchronous control.
Monads are a pattern for composing operations.
Think of it like this:
Instead of:
if x is not None:
y = f(x)
if y is not None:
z = g(y)
# Notice that you'll get an UnboundLocalError if y was None and you reference z later. Hence, you'll probably initialize z to None
You write:
z = Maybe(x).bind(f).bind(g)
The Maybe
monad wraps the value and abstracts the null-checking logic. You define the computation once — the monad handles whether it proceeds or short-circuits.
That’s the magic of monadic composition: composability with context-awareness.The context here being nullable/error’d values.
What Does This Look Like in Python?
Here’s a sketch of a Maybe
monad:
class Maybe:
def __init__(self, value):
self.value = value
def bind(self, func):
if self.value is None:
return Maybe(None)
return Maybe(func(self.value))
def get_or(self, default):
return self.value if self.value is not None else default
Now instead of deeply nested checks, you write:
result = (
Maybe(user)
.bind(lambda u: u.profile)
.bind(lambda p: p.avatar)
.bind(lambda a: a.url)
.get_or(default_url) # "unwrap" the monadic context.
)
This is explicit, readable, and avoids new syntax. Even more importantly it doesn’t just solve null-checking. The same composition style could be used for logging (Writer
monad), error handling (Either
), async pipelines, and more.
Why Python Hasn’t Gone This Route (Yet)
So why hasn't monadic composition gained traction in Python?
No syntax sugar. In Haskell,
do
-notation turns nested binds into readable, sequential code. Python lacks an equivalent.No standard library support. Python doesn’t ship with
Maybe
orResult
. Instead, devs rely on third-party libraries liketoolz
,returns
, orpymonad
.A preference for concrete, incremental features. Python tends to adopt narrow, practical features (like
:=
ormatch
) rather than deep abstractions from category theory.
That said, there’s nothing stopping developers from using monadic patterns today. Community projects like ilyavf/monadic-composition demonstrate how real-world code becomes cleaner and more expressive when composed this way.
Final Thoughts: From PEP 505 to a Better Future
PEP 505 was a pragmatic attempt at solving a real problem. But it was ultimately a band-aid on a broader wound: the need for clean, composable ways to manage context in our programs.
Monadic composition is that solution.
It may not be built into the language. It may never get a PEP. But as Python continues to evolve, borrowing the spirit of monads — even without the syntax — could lead to more powerful, elegant codebases.
Until then, maybe it’s time we wrote our own Maybe
.
References
📝 PEPs and Official Python Docs
PEP 505 – None-aware operators
📚 GitHub Repositories
ilyavf/monadic-composition
— A talk and code examples on monadic composition in real-world programming.TRCYX/py_monad_do
— A library implementing Haskell-style do-notation for monads in Python.dry-python/returns
— A popular library for typed functional programming in Python, including monadic types.
📦 Python Libraries
pymonad on PyPI
— Monadic constructs for Python, includingMaybe
,Either
, andState
.monad-do on PyPI
— A library to use generator-based do-notation in Python.
📖 Articles and Learning Resources
Monadic Composition and Kleisli Arrows – FreeCodeCamp
— A deeper look at monadic composition using function chaining.Monads in Python – Software Patterns Lexicon
— Patterns and idioms for implementing and understanding monads in Python.Monads in Python with PyMonad – Miguel Farrajota
— A tutorial on using the PyMonad library.