PEP 661: Sentinel Values

I like Paul’s suggestion (opt: int = Sentinel) for the same reason I liked opt: int = None: conciseness without sacrificing readability. That said, as Petr points out, opt: int = None was removed from PEP 484 and I don’t think we should be inconsistent between these two cases.

3 Likes

Perhaps Optional should be extended (in the type checker’s logic, not sure any runtime changes are necessary) to allow sentinel types other than None?

It seems like the use case for sentinels is the same as for Optional for the most part, you just need a unique sentinel if None might be a valid part of the user data. So you don’t need a special sentinel for opt: int = Sentinel (since you can use opt: Optional[int] = None there safely), but you do for opt: T = Sentinel because T might be some collection of types that includes None (e.g. Optional[int]).

Maybe the specific sentinel type could even be inferred from the default argument? I’m not sure about that.

I think __bool__ is also important. Sentinels often represent “empty” values that are convenient to handle as falsy, e.g.
def f(value: List | None | Unknown): return value or []. Something like Unknown = sentinel("Unknown", bool=False) would be very useful and convenient.

Use case example
NoAnswer = sentinel("NoAnswer", False)

@dataclass
class ClientSurveyResponse:
    interesting_items: List[Item] | None | NoAnswer = NoAnswer
    # None = no particular preference, [] = interested in nothing
    ...
    
    def items_proposable_to_client(self):
        return [item for item in self.interesting_items or [] if item in inventory]

The alternative is type(Unknown).__bool__ = lambda self: False, but it appears to me that “empty” sentinels are extremely common – the overwhelming majority even, based on this list. Perhaps it should be the default.


P.S. how about the auto-naming syntax from the PyPI’s sentinel package? Something like

MySentinel = sentinel.create()
print(MySentinel)  # prints `MySentinel`
3 Likes