Python Type Hints - Use Cases for the types Module
Writing type hints gives us some familiarity with the
typing module. But Python also includes the similarly-named
types module, which can also come in handy. Let’s look at the history of these two modules, some use cases of
types, and one way in which it’s not so useful.
types came first, by a long way.
types had its first commit in 1994 from Guido van Rossum. The module allows us to access special types that we cannot otherwise reference without a little “gymnastics”. For example,
None is a special singleton made by the Python interpreter’s machinery, so there’s not normally a name bound to its type. But we can find a reference with the
type() built-in like so:
NoneType = type(None)
…and indeed this is what
types module does, for
None and many other types.
typing module was added following the big type hints proposal, PEP 484. Its first commit was in 2015, again by Guido van Rossum. It stores many different tools for writing type hints, and has evolved a lot in recent versions alongside the story for type hints.
types.ModuleType is the type of a Python module. This can come in useful when dealing with dynamic imports, such as the references in Django’s settings files. For example, we could write a function to import a list of modules by name like so:
from __future__ import annotations from importlib import import_module from types import ModuleType def load_modules(names: list[str]) -> list[ModuleType]: return [import_module(name) for name in names] modules = load_modules(["sys", "os.path", "pathlib"])
types.TracebackType represents a traceback. In most programs these are only constructed by Python’s exception-handling machinery. (Indeed, the ability to construct tracebacks directly was only added in Python 3.7.)
In code handling tracebacks, we may need to reference
TracebackType in our type hints. For example, in the previous post How to Type a Context Manager, we needed to accept a traceback in
from __future__ import annotations from types import TracebackType class MyContextManager: def __enter__(self) -> None: pass def __exit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, ) -> None: pass
Not so useful:
Some types in
types having typing equivalents that take type parameters to be more specific. These equivalents started in the
typing module, but in version 3.9 they moved to combine with the versions in
types types and their equivalents are:
types.FunctionType: the type of a normal function as created with
collections.abc.Callableis the type for any callable, and takes parameters for the argument and return types.
types.GeneratorType: the type of a generator, as created by calling a generator function.
collections.abc.Generatoralso represents the type of a generator, with parameters for the yield, send, and return types.
types.CoroutineType: the type of a coroutine, as created by calling an
collections.abc.Coroutinealso represents the type of a coroutine, again with parameters for the yield, send, and return types.
With type hints these
types versions are not so useful, as Mypy doesn’t recognize them.
For example, take this program:
from __future__ import annotations from types import FunctionType def f() -> int: return 12 x: FunctionType = f
We try to store a function
f in variable
x, which we declare as containing a function. This works perfectly fine at runtime, but Mypy thinks it is incorrect:
$ mypy example.py example.py:11: error: Incompatible types in assignment (expression has type "Callable[, int]", variable has type "FunctionType") Found 1 error in 1 file (checked 1 source file)
This behaviour is because Mypy treats functions (of all kinds) specially internally.
Therefore when working with these types, we need to stick to the versions from
collections.abc (or from
typing on Python < 3.9).
Python 3.9 gave us the ability to parametrize built-in types directly, such using
list[int] instead of
typing.List[int]. This is great for writing type hints. But what is the type of
It turns out to be accessible as
types.GenericAlias. For example, we can also construct
list[int] like so:
from types import GenericAlias GenericAlias(list, (int,))
GenericAlias can be useful when introspecting type hints, like the attrs library does.
Learn how to make your tests run quickly in my book Speed Up Your Django Tests.
One summary email a week, no spam, I pinky promise.
- Python Type Hints - How to Avoid “The Boolean Trap”
- Python Type Hints - How to Narrow Types with TypeGuard
- Python Type Hints - How to Upgrade Syntax with pyupgrade
Tags: mypy, python