Skip to content

TypeGuard and Type Narrowing

TypeGuard helps type checkers narrow down types within function bodies, improving type safety.

Mental Model

Type narrowing is how a type checker learns more precise types inside conditional branches. After if isinstance(x, int):, the checker knows x is int in that block. TypeGuard extends this to custom predicates: you write a function that returns bool, annotate it with TypeGuard[T], and the checker treats a True result as proof that the argument is of type T.

Type Narrowing with Isinstance

Use isinstance to narrow types within conditional blocks.

```python from typing import Union

def process_value(value: Union[int, str]) -> str: if isinstance(value, int): # Type checker knows value is int here return f"Integer: {value * 2}" else: # Type checker knows value is str here return f"String: {value.upper()}"

print(process_value(5)) print(process_value("hello")) ```

Integer: 10 String: HELLO

TypeGuard for Custom Type Checks

Create custom type guards for more complex type narrowing.

```python from typing import TypeGuard

def is_list_of_ints(value: list) -> TypeGuard[list[int]]: return all(isinstance(item, int) for item in value)

def process_numbers(value: list) -> int: if is_list_of_ints(value): # Type checker narrows to list[int] return sum(value) else: return 0

print(process_numbers([1, 2, 3])) print(process_numbers([1, "two", 3])) ```

6 0


Exercises

Exercise 1. Write a type guard function is_str_list(val: list[object]) -> TypeGuard[list[str]] that checks whether all elements in a list are strings. Use it to narrow the type before calling .upper() on each element.

Solution to Exercise 1

```python from typing import TypeGuard

def is_str_list(val: list[object]) -> TypeGuard[list[str]]: return all(isinstance(item, str) for item in val)

def process(data: list[object]) -> None: if is_str_list(data): # type checker knows data is list[str] here for item in data: print(item.upper())

process(["hello", "world"]) ```


Exercise 2. Explain why a regular isinstance check is sometimes insufficient for type narrowing with complex types, and how TypeGuard solves this.

Solution to Exercise 2

isinstance(x, list) tells the type checker that x is a list, but not what it contains. You cannot write isinstance(x, list[str]) at runtime. TypeGuard lets you write a custom function that performs the element-level check and tells the type checker the precise generic type.


Exercise 3. Write a function is_positive_int(x: int | str) -> TypeGuard[int] that returns True only if x is a positive integer. Then use it in an if block where the type checker narrows x to int.

Solution to Exercise 3

```python from typing import TypeGuard

def is_positive_int(x: int | str) -> TypeGuard[int]: return isinstance(x, int) and x > 0

def process(val: int | str) -> None: if is_positive_int(val): # type checker narrows val to int print(val + 1) else: print("Not a positive int")

process(5) # 6 process("hello") # Not a positive int ```


Exercise 4. Compare TypeGuard with TypeIs (Python 3.13+). What is the key difference in how they narrow types?

Solution to Exercise 4

TypeGuard[T] tells the type checker that if the function returns True, the argument has type T. However, the else branch does not get narrowed — the original type is preserved.

TypeIs[T] (Python 3.13+) provides symmetric narrowing: in the True branch the type narrows to T, and in the False branch it narrows to the complement. This enables more precise type checking in both branches.