Description
I repeatedly encounter code that uses a singleton instance that is only used to mark some special condition. Here is an example:
_empty = object()
def f(x=_empty):
if x is _empty:
# default argument value
return 0
elif x is None:
# argument was provided and it's None
return 1
else:
# non-None argument provided
return x * 2
Currently PEP 484 doesn't provide primitives for type checking such code precisely. Workarounds such as casts or declaring the type of the special object as Any
are possible. We can't use the type object
in a union type, since that would cover all possible values, as object
is the common base class of everything.
We could refactor the code to use a separate class for the special object and use isinstance
to check for the special value, but this may require changing the code in non-trivial ways for larger examples and carries some runtime overhead as isinstance
checks are slower than is
operations. Example:
from typing import Union
class Empty: pass
_empty = Empty()
def f(x: Union[int, None, Empty] = _empty) -> int:
# use isinstance so that type checkers can recognize that we are decomposing a union
if isinstance(x, Empty):
# default argument value
return 0
elif x is None:
# argument was provided and it's None
return 1
else:
# non-None argument provided
return x * 2
A potentially better way is to support user-defined types that only include a single instance. This can be seen as a generalization of the None
type. Here is a potential way to use this to type check the example:
from typing import SingletonType, Union
_empty = object()
Empty = SingletonType('Empty', _empty)
def f(x: Union[int, None, Empty] = _empty) -> int:
if x is _empty:
# default argument value
return 0
elif x is None:
# argument was provided and it's None
return 1
else:
# non-None argument provided
return x * 2
The final example is better than second example in a few ways:
- It's closer to the original code.
- It's as fast as the original code (modulo one-time overhead due to type annotations).