Typing Best Practices¶
Introduction¶
Over time, some best practices have proven themselves as useful when working with type hints in Python. Not all practices are applicable in all situations and some practices come down to personal style and preference, but they are a good default set of recommendations to fall back to, unless there is a specific reason to deviate.
These best practices are constantly evolving, especially as the typing capabilities and ecosystem grow. So expect new best practices to be added and existing best practices to be modified or even removed as better practices evolve. That is why we would love to hear from your experiences with typing. Please see Discussions and Support on how to join the discussion.
Typing Features¶
Type Aliases¶
Use TypeAlias
for type aliases (but not for regular aliases).
Yes:
_IntList: TypeAlias = list[int]
g = os.stat
Path = pathlib.Path
ERROR = errno.EEXIST
No:
_IntList = list[int]
g: TypeAlias = os.stat
Path: TypeAlias = pathlib.Path
ERROR: TypeAlias = errno.EEXIST
Ergonomic Practices¶
Using Any
and object
¶
Generally, use Any
when a type cannot be expressed appropriately
with the current type system or using the correct type is unergonomic.
If a function accepts every possible object as an argument, for example
because it’s only passed to str()
, use object
instead of Any
as
type annotation. Similarly, if the return value of a callback is ignored,
annotate it with object
:
def call_cb_if_int(cb: Callable[[int], object], o: object) -> None:
if isinstance(o, int):
cb(o)
Arguments and Return Types¶
For arguments, prefer protocols and abstract types (Mapping
,
Sequence
, Iterable
, etc.). If an argument accepts literally any value,
use object
instead of Any
.
For return values, prefer concrete types (list
, dict
, etc.) for
concrete implementations. The return values of protocols
and abstract base classes must be judged on a case-by-case basis.
Yes:
def map_it(input: Iterable[str]) -> list[int]: ...
def create_map() -> dict[str, int]: ...
def to_string(o: object) -> str: ... # accepts any object
No:
def map_it(input: list[str]) -> list[int]: ...
def create_map() -> MutableMapping[str, int]: ...
def to_string(o: Any) -> str: ...
Maybe:
class MyProto(Protocol):
def foo(self) -> list[int]: ...
def bar(self) -> Mapping[str, str]: ...
Avoid union return types, since they require isinstance()
checks.
Use Any
or X | Any
if necessary.
Stylistic Practices¶
Shorthand Syntax¶
Where possible, use shorthand syntax for unions instead of
Union
or Optional
. None
should be the last
element of an union.
Yes:
def foo(x: str | int) -> None: ...
def bar(x: str | None) -> int | None: ...
No:
def foo(x: Union[str, int]) -> None: ...
def bar(x: Optional[str]) -> Optional[int]: ...
def baz(x: None | str) -> None: ...
Types¶
Use float
instead of int | float
.
Use None
instead of Literal[None]
.
Built-in Generics¶
Use built-in generics instead of the aliases from typing
,
where possible.
Yes:
from collections.abc import Iterable
def foo(x: type[MyClass]) -> list[str]: ...
def bar(x: Iterable[str]) -> None: ...
No:
from typing import Iterable, List, Type
def foo(x: Type[MyClass]) -> List[str]: ...
def bar(x: Iterable[str]) -> None: ...