PEP 661: Sentinel Values

That use of __bool__ was ordinary in the past, but now it’s wrong because it ignores modern type checking. You must at least narrow the return type for __bool__ to be Literal[False]:

    def __bool__(self) -> Literal[False]:
        return False

To be clear, whenever I impulsively say “abusing customization of bool” I specifically mean the following:

  • New types using their own truthiness as a sorting category while also being mixed with other unknown types. There is a reason that if x is not None: is common, thus the example MissingType is typically checked with x is MISSING or isinstance(x, MissingType) rather than if x: which is long known to cause insidious issues with generic unions of types. If you break this rule then it should be for a well-documented reason rather than laziness or inertia. The dangerous allure of if x: is why I’ve previously suggested banning truthiness from sentinels.
  • Types with constant truthiness which are not detected by a type checker, return type hints must be Literal for these otherwise one has made a runtime-only pattern.
  • Types which disallow truth tests which are not caught by a type checker. A “not allowed to call this function return type” is still being discussed and I expect Numpy to use them once they’re added. Until then these issues are annoyingly silent until runtime.

Breaking these rules outside of a library might be okay if the only reviewer is oneself but as a library author I always default to the context of writing a library and having high standards for the API. When I see MissingType I think about how an empty string referring to the working directory would have the same truthiness as it as well as other worst case scenarios, but you’re likely to be using MissingType in a less speculative environment.

2 Likes