Here’s my implementation of customizable truthiness. Like before this really just reveals to me that customizable truthiness as a feature might be too overly engineered and complex:
Sentinel generic boolean return types
The Sentinel class can have Generic with a TypeVar holding the return type for __bool__ with the type being one of Literal[True] | Literal[False] | Never. Overloads determine which return type is assigned.
One thing I’m worried about is if the use of Generic here would interfere with type-hinting internally. Otherwise this supports type-hinted conversions to boolean literals without special casing.
from types import NotImplementedType
from typing import Any, Generic, Literal, Never, TypeVar, overload
_BoolReturnType = TypeVar("_BoolReturnType")
class Sentinel(Generic[_BoolReturnType]):
@overload
def __new__(cls, *, bool: NotImplementedType) -> Sentinel[Never]: ...
@overload
def __new__(cls, *, bool: Literal[True]) -> Sentinel[Literal[True]]: ...
@overload
def __new__(cls, *, bool: Literal[False]) -> Sentinel[Literal[False]]: ...
def __new__(cls, *, bool: bool | NotImplementedType) -> Sentinel[Any]:
self = super().__new__(cls)
self._bool = bool
return self
def __bool__(self: Sentinel[_BoolReturnType]) -> _BoolReturnType:
if self._bool is NotImplemented:
raise TypeError
return self._bool
I’ve used a bool keyword-only parameter over bool_value, truthiness, is_true, and others. There’s little ambiguity when the options are bool=True, bool=False, and bool=NotImplemented passed to a Sentinel constructor. It’s also less to type out.
I use bool=NotImplemented as the value which raises an error since it’s more explicit than bool=None which might imply that a default value is being used or that None’s truthiness is used. bool=NotImplemented sets the __bool__ return type to Never and raises an error when truthiness is tested.
The main issue with customizable truthiness is that there are no good examples of using it. Any example I’ve seen or tried to make myself was always too forced.
Sentinel types are not in a vacuum, they are mixed with other types and that will determine what a truth test is actually checking for:
- Sentinel mixed with native sentinels:
None,False,True,Ellipsis,NotImplemented - Sentinel mixed with number types:
int,float - Sentinel mixed with various containers:
str,list,tuple - Sentinel mixed with custom classes with default truth behavior (this case is sometimes abused by truth-testing
None) - Sentinel mixed with objects with unknown behavior
Many examples of sentinels have falsy connotations but I can’t imagine a truth test being valid for the examples above unless it is being passed to third-party code that is already abusing truthiness (and thus would break with simple cases of passing number or container types as some have already stated). Most of the time one would use a predicate function which will handle expected sentinels in situations where truthiness is theoretically needed.
At this point I’m convinced that the optimal solution is to treat conversions to bool as an accident to be avoided, enforced with the following dunder method:
def __bool__(self) -> Never:
raise TypeError