TypeGuard
Imagine you are writing a data pipeline, one function is for processing a list of objects. If the list of objects are all strings or integers then you have separate methods for resolving them, else you mark it as invalid and discard it. An initial reasonable solution is:
def process_str(data: list[str]) -> None:
...
def process_int(data: list[int]) -> None:
...
# Process data if its either a string or a integer
def process_data(data: list[object]) -> None:
if all(isinstance(x, int) for x in data):
process_int(data)
elif all(isinstance(x, str) for x in data):
process_str(data)
else:
print("Invalid data, neither str nor int")
The problem is, this solution will still cause the typechecker to be
unhappy as the inputs for both process_int and
process_str
will still be typed as list[object]. For mutable generics
like list, subtypes are invariant, so list[int] is not a
subtype of list[object], thus invariance exists to protect
the container.
Instead use TypeGuard, which is a special typing construct
which can narrow the type of the object through a function. Recommended to
narrow the broadest scope of what the object could be, to the narrowest
scope. In essence, return False if it's not the correct type so
TypeGuards would look like this:
from typing import TypeGuard, Iterable
def is_list_of_ints(obj: Iterable[object]) -> TypeGuard[list[int]]:
if not isinstance(obj, list):
return False
return all(isinstance(x, int) for x in obj)
def is_list_of_strs(obj: Iterable[object]) -> TypeGuard[list[str]]:
if not isinstance(obj, list):
return False
return all(isinstance(x, str) for x in obj)
Now, given these TypeGuards you can rewrite the above function
as such:
# Process data if its either a string or a integer
def process_data(data: Iterable[object]) -> None:
if is_list_of_ints(data):
process_int(data) # Type checker knows data is list[int] here!
elif is_list_of_strs(data):
process_str(data) # Type checker knows data is list[str] here!
else:
print("Invalid data, neither str nor int")
TypeGuard is also useful in checking for narrowing down typing
to exceptions and validating against a TypeAlias.
Advantages of TypeGuard
- Type Safety: Provides compile-time guarantees that runtime checks match type annotations
- Better IDE Support: IDEs can understand the narrowed types and provide accurate autocomplete
- Cleaner Code: Separates type checking logic into reusable functions
- Documentation:
TypeGuardfunctions clearly document what conditions narrow types
Questions or feedback? Feel free to reach out!