Alex Lin Wang 王帅

TypeGuard

August 27, 2025 • 7 min read
Understanding Python's TypeGuard for type narrowing and better type safety in data processing pipelines.

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: TypeGuard functions clearly document what conditions narrow types

Questions or feedback? Feel free to reach out!