Python Type Hints - How to Narrow Types with TypeGuard

I previously covered type narrowing using isinstance()
, assert
, and 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 isinstance()
:
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 TypeGuard[T]
, where True
means the first argument has type T
, and 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)
is_str_list()
returns True
if the given list contains only strings. We tell Mypy this can narrow the type of value
to list[str]
with the TypeGuard
return type.
Running Mypy on this file, we see this output from the reveal_type()
calls:
$ 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 str
s 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.
Related posts: