Sometimes your code needs to pass along a signal, like “no argument was passed” or “the stream has ended,” in the same place where it normally receives data. Any ordinary value you pick for that job, even None, might also show up as real data. That’s why, in Python, you’ve probably written or seen something like _MISSING = object() to create a sentinel value that your code treats as a signal rather than as data.
That idiom works, but its default representation clutters function signatures, provides no useful information to static type checkers, and fails identity checks after copying. Other approaches have limitations, too. To address all of these drawbacks at once, Python 3.15 adds a built-in sentinel type.
By the end of this tutorial, you’ll understand that:
- Python 3.15 adds
sentinelto the built-ins through PEP 661, so you don’t need to import the type. - A sentinel prints its own name, so it reads clearly in function signatures,
help(), andinspect.signature(). - Sentinels keep their identity through
copy(),deepcopy(), and apickleround trip. - A sentinel works as a type hint, following the precedent set by
None. Noneremains the recommended default unlessNoneis legitimate data in your code, in which case you should use a sentinel.
To start, you’ll look at what a sentinel is and the existing options for creating one. Then you’ll meet Python 3.15’s sentinel built-in and work through the issues it solves.
Note: The code examples in this tutorial were tested with Python 3.14 and 3.15.0rc2, the latest Python 3.15 pre-release at the time of writing.
Get Your Code: Click here to download the free sample code you’ll use to create sentinel values with Python 3.15’s new sentinel built-in and retire the unreadable object() idiom.
Understand Sentinel Values in Python
Before you learn about the new built-in sentinel type, you’ll look at the problem it addresses in earlier Python versions. In this section, you’ll learn what a sentinel value is, recognize the ones you’ve been using for years without naming them, and see where existing approaches fall short. This context explains why Python adds sentinel to its built-in types.
Know What a Sentinel Value Is
A sentinel value is a value that an algorithm treats as a signal rather than as data. It marks a condition such as “terminating the iteration,” “shutting down the worker thread,” or “nothing was passed.” A sentinel is a marker that your code branches on, and it doesn’t carry any information about the data you’re processing.
Python uses sentinel values in many situations. For example, when you search a string and the target substring isn’t there, you get -1 back:
>>> "Hello, World!".find("z")
-1
In this example, -1 tells you that the substring isn’t there. In other words, your code should read it as a signal rather than as an index pointing to the target substring.
Note: The example above shows that a sentinel can be an ordinary value that you’ve given a special meaning. The drawback is that the value can collide with your data. For example, -1 collides with negative indices. You’ll learn more about this in the section Explore Some Limitations of Sentinels in Python.
A sentinel can also be a unique object that you create for this purpose, ensuring that nothing else can ever equal it.
The essential idea is that a sentinel value should stay distinct from every legitimate value your data can take. This way, a loop over your data can check each value and branch when it encounters the sentinel:
Now that you know what a sentinel value is, you can start spotting the ones that Python has been handing you all along.
Spot the Sentinels Python Already Uses
You’ve probably been using sentinels since your early Python programs. Using None as the default value for optional arguments is a common example. It typically means “no value was passed.” In a data context, it may mean “no data.”
In the standard library, you’ll also find a variety of sentinels. Here are four modules and some of their sentinel values:
>>> import configparser
>>> configparser._UNSET
<object object at 0x7f6d134cc6f0>
>>> import inspect
>>> inspect.Parameter.empty
<class 'inspect._empty'>
>>> import typing
>>> typing.NoDefault
typing.NoDefault
>>> import unittest.mock
>>> unittest.mock.DEFAULT
sentinel.DEFAULT
Each of these four modules uses a different approach to sentinels:
configparseruses a bareobjectinstance.inspectuses a class directly, without ever instantiating it.typinguses an instance of its own dedicated type,NoDefaultType.unittest.mockuses an instance of a purpose-built_SentinelObjectclass.