Type system concepts¶
Union types¶
Since accepting a small, limited set of expected types for a single
argument is common, the type system supports union types, created with the
|
operator.
Example:
def handle_employees(e: Employee | Sequence[Employee]) -> None:
if isinstance(e, Employee):
e = [e]
...
A type factored by T1 | T2 | ...
is a supertype
of all types T1
, T2
, etc., so that a value that
is a member of one of these types is acceptable for an argument
annotated by T1 | T2 | ...
.
One common case of union types are optional types. By default,
None
is an invalid value for any type, unless a default value of
None
has been provided in the function definition. Examples:
def handle_employee(e: Employee | None) -> None: ...
A past version of this specification allowed type checkers to assume an optional
type when the default value is None
, as in this code:
def handle_employee(e: Employee = None): ...
This would have been treated as equivalent to:
def handle_employee(e: Employee | None = None) -> None: ...
This is no longer the recommended behavior. Type checkers should move towards requiring the optional type to be made explicit.
Support for singleton types in unions¶
A singleton instance is frequently used to mark some special condition,
in particular in situations where None
is also a valid value
for a variable. Example:
_empty = object()
def func(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:
return x * 2
To allow precise typing in such situations, the user should use
a union type in conjunction with the enum.Enum
class provided
by the standard library, so that type errors can be caught statically:
from enum import Enum
class Empty(Enum):
token = 0
_empty = Empty.token
def func(x: int | None | Empty = _empty) -> int:
boom = x * 42 # This fails type check
if x is _empty:
return 0
elif x is None:
return 1
else: # At this point typechecker knows that x can only have type int
return x * 2
Since the subclasses of Enum
cannot be further subclassed,
the type of variable x
can be statically inferred in all branches
of the above example. The same approach is applicable if more than one
singleton object is needed: one can use an enumeration that has more than
one value:
class Reason(Enum):
timeout = 1
error = 2
def process(response: str | Reason = '') -> str:
if response is Reason.timeout:
return 'TIMEOUT'
elif response is Reason.error:
return 'ERROR'
else:
# response can be only str, all other possible values exhausted
return 'PROCESSED: ' + response