Typing Best Practices


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).


_IntList: TypeAlias = list[int]
g = os.stat
Path = pathlib.Path


_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):

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.


def map_it(input: Iterable[str]) -> list[int]: ...
def create_map() -> dict[str, int]: ...
def to_string(o: object) -> str: ...  # accepts any object


def map_it(input: list[str]) -> list[int]: ...
def create_map() -> MutableMapping[str, int]: ...
def to_string(o: Any) -> str: ...


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.


def foo(x: str | int) -> None: ...
def bar(x: str | None) -> int | None: ...


def foo(x: Union[str, int]) -> None: ...
def bar(x: Optional[str]) -> Optional[int]: ...
def baz(x: None | str) -> None: ...


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.


from collections.abc import Iterable

def foo(x: type[MyClass]) -> list[str]: ...
def bar(x: Iterable[str]) -> None: ...


from typing import Iterable, List, Type

def foo(x: Type[MyClass]) -> List[str]: ...
def bar(x: Iterable[str]) -> None: ...