TypeGuard in Python
TypeGuard is a typing feature that lets you teach the type checker how a boolean-returning function narrows types when it returns True.
It was introduced in PEP 647 and is available from:
- Python 3.10+ via typing.TypeGuard
- Python 3.8–3.9 via typing_extensions.TypeGuard
The Problem TypeGuard Solves
Consider this common pattern:
from typing import Union
def is_str(x: Union[str, int]) -> bool:
return isinstance(x, str)
def f(x: Union[str, int]) -> None:
if is_str(x):
reveal_type(x) # still Union[str, int]
Even though you know x is a str, the type checker does not. Why? A normal bool return does not convey any information about what changed when the function returned True.
Enter TypeGuard
TypeGuard lets you say, if this function returns True, then the argument is of a specific type.
from typing import TypeGuard, Union
def is_str(x: Union[str, int]) -> TypeGuard[str]:
return isinstance(x, str)
def f(x: Union[str, int]) -> None:
if is_str(x):
reveal_type(x) # str
Now the type checker trusts the narrowing.
Key points:
- The return type must be
TypeGuard[...].
- The function must return
bool.
The narrowed type applies only when the function returns True. Note that type narrowing is one-way (see later). This is essentially a customisable version of isinstance, e.g.
def is_non_empty_str(x: object) -> TypeGuard[str]:
return isinstance(x, str) and len(x) > 0
Narrowing to Protocols
TypeGuard works well with structural typing.
from typing import Protocol, TypeGuard
class HasClose(Protocol):
def close(self) -> None: ...
def has_close(obj: object) -> TypeGuard[HasClose]:
return hasattr(obj, "close")
def f(x: object):
if has_close(x):
x.close() # type-safe
This is extremely useful for duck-typed APIs.
Narrowing Containers
from typing import Iterable, TypeGuard
def all_strings(xs: Iterable[object]) -> TypeGuard[list[str]]:
return isinstance(xs, list) and all(isinstance(x, str) for x in xs)
def f(xs: Iterable[object]):
if all_strings(xs):
xs.append("hello") # ok
⚠️ Be careful: you are promising that all elements satisfy the type.
TypeGuard and runtime behaviour
TypeGuard ONLY informs the typechecker. If your custom check is wrong, your program may crash at runtime.
TypeGuard and Else Branches
We discussed that type guards only narrow one way.
def is_str(x: object) -> TypeGuard[str]:
return isinstance(x, str)
def f(x: str | int):
if is_str(x):
reveal_type(x) # str
else:
reveal_type(x) # str | int (unchanged!)
If you want two-sided narrowing you have to use multiple guards.
TypeGuard vs type is
Python 3.13 introduces type is (PEP 742). This is exactly the same, just more explicit.
def is_str(x: object) -> type is str:
return isinstance(x, str)
TypeGuard with Generics
from typing import TypeVar, Iterable, TypeGuard
T = TypeVar("T")
def is_non_empty(xs: Iterable[T]) -> TypeGuard[list[T]]:
return isinstance(xs, list) and len(xs) > 0
Overusing TypeGuard
Put simply, if isinstance works, use it instead.