Python Type Hints - How to Narrow Types with TypeGuard
I previously covered type narrowing using
Literal. In today’s post we’ll cover
TypeGuard, a new special type that allows us to create custom type narrowing functions.
TypeGuard was defined in PEP 647, and is available on Python 3.10+, or on older versions from
typing-extensions. Guido van Rossum added support to Mypy in version 0.900, which was released yesterday.
Recall that type narrowing uses particular expressions to infer that, in a given block, a variable has a more limited type than its definition. For example, using
from __future__ import annotations name: str | None if isinstance(name, str): # name must be 'str' ... else: # name must be None ...
Type checkers, including Mypy, support a limited number of expressions, such as
if isinstance(...). But the number of potentially type-narrowing expressions is infinite, especially for parameterized types such as containers.
TypeGuard allows us to write type any expression and communicate to our type checker that it narrows types.
A type narrowing function is one that accepts at least one argument and returns a
bool. Instead of marking the return type as
bool, we use
True means the first argument has type
False means it does not. Take this example, adapted from the PEP:
from __future__ import annotations from typing_extensions import TypeGuard def is_str_list(value: list[object]) -> TypeGuard[list[str]]: """Are all list items strings?""" return all(isinstance(x, str) for x in value) x: list[object] reveal_type(x) if is_str_list(x): reveal_type(x)
True if the given list contains only strings. We tell Mypy this can narrow the type of
list[str] with the
TypeGuard return type.
Running Mypy on this file, we see this output from the
$ mypy --strict example.py example.py:13: note: Revealed type is "builtins.list[builtins.object]" example.py:15: note: Revealed type is "builtins.list[builtins.str]"
The second note shows us that within the
if block, Mypy knows
x must be a list of strings. This allows us to use the list items as
strs there without any errors. Great!
TypeGuard is flexible as it allows us to write arbitrary code to narrow expressions. It does force us to move even short expressions into separate functions, but that can often a good thing for our code’s readability.
Because there are infinite possible expressions, type checkers cannot validate that our expressions line up with the guarded types. So we need to write our
TypeGuard functions with care, and test them thoroughly.
PEP 647 also shows generic
TypeGuard functions with
TypeVar, but when I tried out the examples, I found Mypy 0.901 does not yet support them. There are a few open issues for
TypeGuard so it seems Mypy could use our contributions here!
Make your development more pleasant with Boost Your Django DX.
One summary email a week, no spam, I pinky promise.
- Python Type Hints - How to Narrow Types with isinstance(), assert, and Literal
- Python Type Hints - How to Specify a Class Rather Than an Instance Thereof
Tags: mypy, python