Date: 2023-27-04
AnyPrefer 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.
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.
typing.TYPE_CHECKING to handle circular
dependenciesYou 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 CircularDependencyThingtyping.TYPE_CHECKING to type mixinsTyping 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 basetyping.Protocol to allow duck typingUse 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.sumIn 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.
typing.TypedDict to type-check dictionariesDictionaries 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.
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})typing.Generator to type context managersContext 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`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.
typing.reveal_type to understand types of third-party
librariesUse 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])"mypy-play.net to toy around with typesWhen 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.
stubgen to quickly
generate types for a untyped libraryNot 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 stubsSet 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.
py.typed marker in your libraryWhen 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.typedSince 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.pysetup(
package_data={"mypackage": ["py.typed"]},
)setup.cfg[options.package_data]
mypackage =
py.typed