Much Ado About None: Sentinel Values in Python

Hacker News Top News

Summary

A blog post and talk recap exploring the ambiguity of `None` in Python and how sentinel values solve it, highlighting that PEP 661 was accepted and a `sentinel` built-in is now available in Python 3.15 with type-narrowing support.

No content available
Original Article
View Cached Full Text

Cached at: 10/31/26, 09:22 PM

# Much Ado About None: Sentinel Values in Python Source: [https://florianwilhelm.info/2026/10/sentinel_values/](https://florianwilhelm.info/2026/10/sentinel_values/) `None`is probably the most overloaded value in Python\. We use it as a Swiss Army knife to express that something is*missing*,*unset*, that an*error*occurred, or simply as an actual value\. In my talk about sentinel values, which I presented this year at[PyConDE/PyData Berlin 2026](https://2026.pycon.de/talks/88TTRY/),[EuroPython 2026](https://ep2026.europython.eu/session/the-problem-with-none-sentinel-values-and-the-semantics-of-absence/)and, most recently, the[Swiss Python Summit 2026](https://talks.python-summit.ch/swiss-python-summit-2026/talk/JHJ3XY/), I take a closer look at why this ambiguity bites us and what to use instead\. The problem shows up in many places\. A simple cache returns`None`both for a missing key and for a key that maps to`None`\. Does`timeout=None`in a configuration update mean “no timeout” or “keep the current value”? And a function that swallows a`TimeoutError`and returns`None`uses it as an error signal\. Sentinel values resolve this by being a guard*outside*the domain of valid values, so they can never collide with an actual value\. Python itself relies heavily on sentinels, hidden in plain sight:`Ellipsis`skips dimensions in NumPy slicing and marks stubs, and`NotImplemented`means “find the implementation somewhere else”, which enables double dispatch for binary operators via methods like`\_\_rsub\_\_`\. Pydantic’s`MISSING`distinguishes a field that was explicitly set to`null`from one that was never sent at all, and it has just left the experimental stage in[Pydantic 2\.14](https://pydantic.dev/articles/pydantic-v2-14-release)\. Things get tricky once you create your own sentinels\. The classic`MISSING = object\(\)`works perfectly at runtime but breaks type narrowing in static type checkers\. Workarounds like`TypeIs`guards, magic`Literal`strings or the single\-member`Enum`trick all come with trade\-offs, and identity checks break when sentinels are copied or pickled\. Then came a plot twist\. At PyConDE in April, I had to conclude that[PEP661](https://peps.python.org/pep-0661/), the proposal to standardize sentinels, was deferred\. Only nine days later, the Steering Council accepted it\. Since Python 3\.15,[`sentinel`](https://docs.python.org/3/library/functions.html#sentinel)is a built\-in, and type checkers can narrow it just like`None`: ``` MISSING = sentinel("MISSING") def update_email(user_id: int, email: str | None | MISSING = MISSING) -> None: if email is MISSING: return # nothing to update elif email is None: db.delete_email(user_id) else: db.set_email(user_id, email.lower()) # narrowed to str ``` If you are on an older Python version,[typing\_extensions](https://typing-extensions.readthedocs.io/en/latest/#typing_extensions.sentinel)provides a backport\. Type\-checker support is still catching up, so check your type checker before relying on it\. My main takeaway: be aware of the ambiguity of`None`and think about what you actually want to express\. Use sentinels to resolve the ambiguity, prefer the ones your library already provides, and replace the old`object\(\)`and`Enum`hacks with the new built\-in\. If you are tempted to return`None`instead of raising an exception, my talk on[exceptions and result types](https://florianwilhelm.info/2025/04/evolution_of_goto/)might also interest you\. Here are the[slides](https://florianwilhelm.info/documents/sentinel_values.pdf)from PyConDE, which still describePEP661 as deferred\. The EuroPython recording below is the most up\-to\-date version of the talk and covers the new`sentinel`built\-in\. ---

Similar Articles

two case studies of NaN

Lobsters Hottest

This article explores two unexpected behaviors of NaN in Python and Lua, where implicit assumptions about equality and comparison lead to surprising results like Python's list equality ignoring NaN's self-inequality and Lua's for-loops mishandling NaN as step or limit.

Opaque Types in Python

Hacker News Top

The article explains the opaque data type pattern in Python using typing.NewType to hide internal implementation details, using a ShippingOptions example to show how to provide a minimal public interface.

Are you expected to run five Python type-checkers now?

Hacker News Top

A blog post arguing that Python library maintainers should prioritize running multiple type-checkers on their test suite to ensure public API compatibility, highlighting challenges with compatibility and code pollution.

i've been thinking about null pointers

Lobsters Hottest

The author explores the concept of null pointers, discussing their semantic role and proposing that other invalid pointer values could be used to store additional information, akin to NaN boxing.