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.Callable
is 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.Generator
also 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 def
function.collections.abc.Coroutine
also 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.
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.
Related posts: