Python type hints – How to narrow down the number of types with TypeGuard


I have previously talked about type narrowing with isinstance()
, assert
and Literal
. In today’s post, we’ll look at TypeGuard
a new special type that allows us to create custom type narrowing functions.
TypeGuard
was defined in PEP 647 and is available in Python 3.10+ or older versions from typing-extensions. Guido van Rossum added support to Mypy in version 0.900, which was published the day before.
Recall that type narrowing uses certain expressions to infer that, in a given block, a variable has a more restricted 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
...
Typecheckers, including Mypy, support a limited number of expressions such as if isinstance(...)
. But the number of potentially type-restricting expressions is endless, especially for parameterized types like containers. TypeGuard
allows us to write any type expression and tell our type checker that it narrows them down.
A type narrowing function is one that takes at least one argument and returns bool
. Instead of denoting the return type as bool
we use TypeGuard[T]
where True
means the first argument is of type T
a False
– No. Let’s take this example, adapted from 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 that it can narrow down the type value
before list[str]
using return type TypeGuard
.
Running Mypy on this file, we see the following result call reveal_type()
:
$ 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 that Mypy knows that x
must be a list of strings in a block if
. This allows us to use the elements of the list as str
without any errors. Fine!
TypeGuard
is flexible because it allows us to write arbitrary code to narrow expressions. True, it forces us to wrap even short expressions in separate functions, but this is often useful for code readability.
Since there are an infinite number of possible expressions, type checkers cannot confirm that the expressions we choose match the protected types. Therefore, the functions TypeGuard
should be written with care and tested thoroughly.
PEP 647 also shows common features TypeGuard
with TypeVar
, but when I tried the examples, I found that Mypy 0.901 doesn’t support them yet. For TypeGuard has several open issuesso it looks like Mypy can benefit from our input on their solution!
And keep your types well protected.
We invite everyone to an open lesson on the topic “Introduction to web development with Flask“. In this lesson, we will introduce you to the basics of Flask web development, as well as learn how to create and render page templates. Let’s try to create a Flask application, then create routes, and finally process various HTTP methods in Flask. Registration link.