Python 3.15 New Feature: Is the sentinel Type Better Than None?

Python 3.15 introduces the sentinel type, using named unique sentinels to replace the semantically ambiguous None and bare object().
The article discusses the ambiguity of None in Python: it may simultaneously mean a missing value, an explicit user choice of empty, or invalid input, making it hard to distinguish function return states. Python 3.15 introduces the sentinel type, allowing named unique sentinel objects like missing = sentinel('missing') to mark each business state independently. Compared with the common bare object() sentinel, sentinel keeps uniqueness while providing a readable name and a clear type identity, preventing type checkers from treating the parameter as accepting any type. The article argues the change makes API contracts clearer, especially for libraries, SDKs, and interfaces with default parameters, continuing Python's "explicit is better than implicit" philosophy.
In Python, None is often used to mean "no value," but its semantics are actually quite ambiguous: missing values, the user explicitly choosing empty, invalid input... all of these cases can end up crammed into the same None. The sentinel type introduced in Python 3.15 is designed precisely to liberate these kinds of "state markers" from None and bare object().

Why isn't None enough?
Imagine a user-choice scenario: the user can select a value, or explicitly choose "nothing at all." In the string world, an empty choice can be represented by an empty string; in the numeric world, zero and negative one are both valid values, so they can't be used to represent "empty." So developers usually turn to None.
The problem is that None carries too many meanings—it may mean a value was never provided, it may mean the user deliberately chose empty, or it may mean the input is invalid. When a single function return value needs to distinguish between these states, None loses its expressiveness. The presenter in the video proposes that what we need is a sentinel data type used to define mutually independent states.

A sentinel value is a special marker in programming used to represent some control state outside the normal range of values, such as "not found," "not provided," or "end of loop." It must not be equal to any valid data value, and a typical implementation is to use a unique object. Similar practices already exist in the Python standard library, such as dataclasses.MISSING, inspect.Parameter.empty, and the __sentinel__ of re.Pattern. The core requirements for a sentinel are uniqueness and identifiability: callers only need to compare with is to determine the state, without confusing it with user data. Understanding this makes it clear why using None or -1 as a sentinel introduces ambiguity.
Basic usage of sentinel
Python 3.15 allows you to directly create named sentinel objects, for example missing = sentinel('missing'), and then define another empty = sentinel('empty') for "empty choice." Each sentinel is an independent and unique object that can correspond precisely to one business state.
Before this, the common community alternative was missing = object()—creating a bare object as a marker. Although this approach guarantees uniqueness, it has obvious shortcomings.

Type hints is the static type annotation mechanism introduced in Python 3.5, which works with checkers such as mypy and pyright to find type errors before runtime. Type checkers treat parameters annotated as object as accepting any value, so when a bare object() is used as a sentinel, the function signature cannot express "this only accepts a certain sentinel or a concrete type." PEP 484 provides tools such as typing.Literal, typing.Final, and typing.TypeGuard to describe special values, but until sentinel appeared, there was no language-level sentinel type that was both unique and precisely trackable by the type system. This explains why the new type can improve the type hinting experience.
The old problem with object() as a sentinel
The biggest trouble with using object() as a sentinel shows up in type hints. If you write the type annotation as object or type(missing), the type checker will consider the parameter to accept any type, which amounts to giving up type constraints. In addition, directly printing a bare object yields a meaningless memory address identifier, making debugging unpleasant.

The design goal of sentinel is to make these kinds of markers more convenient: it preserves uniqueness while providing a readable name and having a clear identity in the type system, rather than degenerating into an object that "accepts anything."
repr() and debugging readability: The default string representation of a Python object looks like <object object at 0x7f...>, containing only the type name and memory address, with almost no information for logs and interactive debugging. A good repr() should let developers see the object's identity at a glance, for example sentinel('missing') could display as <sentinel missing>. This is exactly one of the practical improvements of sentinel over bare object(): it gives sentinels their own semantic labels in error messages, debuggers, and the REPL, reducing troubleshooting costs.
Impact on everyday code
The value of this change is not syntactic sugar, but making API contracts clearer. When a function needs to distinguish between "argument not passed," "explicitly passed empty," and "passed an invalid value," in the past you often had to combine None, special strings, or custom classes; now you can express it with a set of semantically clear sentinels. For writing libraries, SDKs, or business interfaces with default parameters, this improvement in expressiveness is especially obvious.
From a design philosophy perspective, sentinel continues Python's pursuit of "explicit is better than implicit": rather than letting None take on multiple responsibilities, give each state a name. For developers who have long suffered from the ambiguity of None, this may be one of the most practical small changes in Python 3.15.
One-line Summary
Python 3.15 introduces the sentinel type, using named unique sentinels to replace the semantically ambiguous None and bare object().
Paragraph Summary
The article discusses the problem of None's ambiguous semantics in Python: it may simultaneously represent a missing value, the user explicitly choosing empty, or invalid input, making it difficult to distinguish different states in function return values. Python 3.15 introduces the sentinel type, allowing the creation of named unique sentinel objects such as missing = sentinel('missing'), providing an independent marker for each business state. Compared with the bare object() sentinel commonly used in the community, sentinel preserves uniqueness while providing a readable name and a clear type identity, preventing type checkers from inferring the parameter as accepting any type. The article argues that the value of this change lies in making API contracts clearer, especially suitable for library, SDK, and interface design with default parameters, continuing Python's design philosophy that "explicit is better than implicit."
Related articles

WorkBuddy Full Guide: How an AI Coworker Takes Over Workplace Automation
What is WorkBuddy? This article analyzes the core capabilities of this full-scenario AI agent: autonomous execution with deliverable results, local file processing, multi-agent parallelism, and workplace automation workflows, helping you judge its essential difference from ordinary AI chat tools.

The Truth About AI Agents: A Hundred Lines of Code + an LLM, Deconstructing the Agent's Underlying Logic
AI agents aren't a new AI species—just a language model plus tools, a loop, and a goal. Here's the underlying logic: how 100 lines of Python hit a 75% fix rate, why the harness beats the model, and Gartner's "agent washing."

AI Agent Deployment: Three Things Developers Should Do
AI Agent deployment doesn't require rushing into multi-agent systems. This article outlines an actionable deployment path and engineering acceptance thresholds for developers, covering human baselines, bounded single-agent scope, and a controlled code-modification loop.