Python typing cheat sheet

Author: Swen Kooij

Date: 2023-27-04

Prefer cast over ignore and Any

Prefer to use typing.cast to get you out of a tricky situation over # type: ignore. Casting the type is often more precise and would still let other errors surface and communicates your intent to other developers.

typing.cast should also be preferred over Any. Any destroys type-checking. Forcing a type through typing.cast is often better as it still offers some type-checking as opposed to Any, which can be passed everywhere and will allow you to access any property without errors. You still get some safety guarentees, even if it’s a hack.

Prefer targetted ignores

If you run mypy with --show-error-codes, you should be able to write # type: ignore[<error code 1>, <error code 2> instead of just # type: ignore. Other errors than the one that you’re trying to ignore will still be reported.

Use typing.TYPE_CHECKING to handle circular dependencies

You might have a circular dependency as a result of trying to properly annotate your code. It’s not uncommon for such dependencies to exist only during type-checking and not at runtime. Nonetheless, type imports are evaluated at runtime as well. To work-around this, type-only imports can be guarded by typing.TYPE_CHECKING so that they are only evaluated when the type checker runs:

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from somemodule import CircularDependencyThing

Use typing.TYPE_CHECKING to type mixins

Typing mixins can be tricky because you often cannot give them a proper base class as that would lead to the diamond problem. This leads to type-checking problems as the type checker does not know what base class you assume it to have. We can solve this by only giving the mixin a base class during type checking:

from typing import TYPE_CHECKING

from django.views import View

if TYPE_CHECKING:
    MyMixinBase = View
else:
    MyMixinBase = object


class MyMixin(MyMixinBase):
    def do_something(self) -> None:
        print(self.request) # is ok, type checker assumes `View` as a base

Use typing.Protocol to allow duck typing

Use typing.Protocol to allow for duck typing. Subclassing typing.Protocol allows you to describe an object without mentioning a specific type.

from typing import Protocol

class Summer(Protocol):
    def sum(self, a: int, b: int) -> int: ...


class Sum1:
    def sum(self, a: int, b: int) -> int:
        return a + b


class Sum2:
    def sum(self, a: int, b: int) -> int:
        return sum([a, b])


def execute_sum(impl: Summer) -> int:
    return impl.sum(1, 1)


execute_sum(Sum1()) # ok because it implements Summer.sum
execute_sum(Sum2()) # ok because it implements Summer.sum

In the example above, Sum1 and Sum2 do not share a base class or implement an interface. They are completely distinct types. With Protocol we have described that we accept any object that implements the sum method and thus, Sum1 and Sum2 can be passed to the execute_sum method.

Use typing.TypedDict to type-check dictionaries

Dictionaries are often used as general-purpose data containers to pass around related data. With typing.TypedDict we can describe what we expect a dictionary to look like. Often we don’t need to annotate the dictionary itself, just functions and classes that expect to be passed a certain type of dictionary.

from typing import TypedDict

class MyDataContainer(TypedDict):
    a: str
    b: int


def print_stuff(data: MyDataContainer) -> None:
    print(data["a"])
    print(data["b"] + 1) # ok because type checker knows `b` is `int`

print_stuff({"a": "hello", "b": 1})

By default, typing.TypedDict does not accept unknown keys. This should be considered a sane default as it prevents accidental typos.

Alternative syntax

If for some reason you need to construct typing.TypedDict types dynamically, an alternative syntax can be used described in PEP-0589:

from typing import TypedDict

MyDataContainer = TypedDict("MyDataContainer", {"a": str, "b": int})

Use typing.Generator to type context managers

Context managers are just generator functions that yield a single value. typing.Generator can be used to annotate the return value of a context manager:

from typing import Generator
from contextlib import contextmanager

@contextmanager
def summing_context_manager(a: int, b: int) -> Generator[int, None, None]:
    yield a + b


with summing_context_manager(1, 1) as result:
    print(result + 1) # ok because `result` is `int`

Read the mypy documentation to type decorators

The Typing decorators section of the mypy documentation explains how to type decorators in various scenarios. I cannot do a better job than the mypy documentation.

Use typing.reveal_type to understand types of third-party libraries

Use typing.reveal_type to discover the type of just about anything. This can be very useful when working with typed third-party libraries.

from typing import reveal_type

print(reveal_type(sum))

The example above prints a notice in the mypy output:

main.py:3: note: Revealed type is "Overload(def (typing.Iterable[builtins.bool], start: builtins.int =) -> builtins.int, def [_SupportsSumNoDefaultT <: builtins._SupportsSumWithNoDefaultGiven] (typing.Iterable[_SupportsSumNoDefaultT`-1]) -> Union[_SupportsSumNoDefaultT`-1, Literal[0]], def [_AddableT1 <: _typeshed.SupportsAdd[Any, Any], _AddableT2 <: _typeshed.SupportsAdd[Any, Any]] (typing.Iterable[_AddableT1`-1], start: _AddableT2`-2) -> Union[_AddableT1`-1, _AddableT2`-2])"

Use mypy-play.net to toy around with types

When typing very pythonic code, it is often useful to build an isolated version of your problem on mypy-play.net to quickly evaluate different approaches against different Python and mypy versions.

Use stubgen to quickly generate types for a untyped library

Not all libraries are typed and not all of them have third-party types in typeshed. Use the stubgen command line tool that ships with mypy to quickly auto-generate types for an untyped library and keep them in your repository till you’ve refined them enough to be contributed to typeshed:

stubgen -p mylibrary -o stubs

Set up mypy to take your stubs into account:

[tool.mypy]
mypy_path = ["stubs"]

This is often better than setting ignore_missing_imports = true for the library as it still allows some type-checking to occur. The auto-generated types aren’t always 100% correct, but the auto-generated stubs are often a good start.

Include the py.typed marker in your library

When you’re developing a library with types, make sure to add the py.typed marker to mark your library as typed. Without this, type-checkers will skip over your library and treat it as untyped.

py.typed is just an empty file that you include in your package. You can create it with the touch command:

touch mypackage/py.typed

Since it is an empty, non-Python file, you must configure your package to include it when bundling it into a wheel or source distribution:

pyproject.toml

[tool.setuptools.package-data]
"mypackage" = ["py.typed"]

setup.py

setup(
    package_data={"mypackage": ["py.typed"]},
)

setup.cfg

[options.package_data]
mypackage =
    py.typed