Tom Kuson

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!

#python

Reply to this post by email ↪