Sentinel values in Python
In Python, we sometimes want to create sentinel values (for example, to act as
a placeholder default value distinct from None). The simplest way to do this
is to create a new object and perform an identity check.
1Unset = object()Let’s say that a can be an int or None or this Unset sentinel value; how
would we type a? Using int | None | object would accept all values due to
the union with object, which we don’t want.
We could try creating a new class just for this sentinel.
1class UnsetT:
2 pass
3
4Unset = UnsetT()However, int | None | Unset wouldn’t narrow to int | None if we performed an
a is not Unset check. Just because a value is not a specific instance of a
type doesn’t mean it cannot be some other instance of that type. We’d have to
perform an isinstance check, which is more expensive and not nice to read.
For a type-correct sentinel value, we can use an enum.Enum value (which is
special-cased by type-checkers). By doing this, type-checkers can rule out the
enumeration type by checking against its one and only member. Using the new
type keyword, we can avoid exposing this implementation detail (though we
unfortunately need separate names for the sentinel instance and its type).
1import enum
2
3class _UnsetT(enum.Enum):
4 UNSET = enum.auto()
5
6Unset = _UnsetT.UNSET
7type UnsetT = _UnsetT
8
9def test(a: int | None | UnsetT = Unset) -> None:
10 if a is Unset:
11 return
12 reveal_type(a) # Revealed type is "builtins.int | None"Until PEP 661 is accepted, this might be the best we can do!