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.
History
Chronologically, 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.
The 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.
ModuleType
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"])
TracebackType
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 __exit__():
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: FunctionType, GeneratorType, and CoroutineType
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 collections.abc. The types types and their equivalents are:
types.FunctionType: the type of a normal function as created withdef.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 anasync deffunction.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).
GenericAlias
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 list[int]?
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.
😸😸😸 Check out my new book on using GitHub effectively, Boost Your GitHub DX! 😸😸😸
One summary email a week, no spam, I pinky promise.
Related posts: