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.
Unset = 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.
class UnsetT:
pass
Unset = 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).
import enum
class _UnsetT(enum.Enum):
UNSET = enum.auto()
Unset = _UnsetT.UNSET
type UnsetT = _UnsetT
def test(a: int | None | UnsetT = Unset) -> None:
if a is Unset:
return
reveal_type(a) # Revealed type is "builtins.int | None"
Until PEP 661 is accepted, this might be the best we can do!