Type Narrowing and `TypeGuard`
Python · Advanced · Run with python3
Explanation
Type narrowing is the process by which a type checker infers a more specific type after a check. `isinstance(x, str)` narrows `x: int | str` to `str` in the `if` branch. `TypeGuard[T]` (PEP 647, Python 3.10+) lets you write custom narrowing functions: a function returning `TypeGuard[SpecificType]` tells the type checker that the return value implies the argument is of `SpecificType`. Python 3.12 added `TypeIs` (PEP 742) as a stricter alternative.
Real-World Scenario
A function receives `data: dict[str, object]` from JSON. After validating with `is_valid_config(data)` (returning `TypeGuard[Config]`), the type checker knows `data` is `Config` — no `cast()` needed, and you get full autocomplete on `Config` attributes. ```python from typing import TypeGuard, Union from dataclasses import dataclass @dataclass class Cat: name: str indoor: bool @dataclass class Dog: name: str breed: str Animal = Union[Cat, Dog] def is_cat(animal: Animal) -> TypeGuard[Cat]: return isinstance(animal, Cat) def describe(animal: Animal) -> str: if is_cat(animal): # Type checker knows animal is Cat here return f"Cat {animal.name}, indoor={animal.indoor}" else: # Type checker knows animal is Dog here return f"Dog {animal.name}, breed={animal.breed}" animals: list[Animal] = [Cat("Whiskers", True), Dog("Rex", "Labrador")] for a in animals: print(describe(a)) ```
Quick Snippet
from typing import TypeGuard, Union
from dataclasses import dataclass
@dataclass
class Cat:
name: str
indoor: bool
@dataclass
class Dog:
name: str
breed: str
Animal = Union[Cat, Dog]
def is_cat(animal: Animal) -> TypeGuard[Cat]:
return isinstance(animal, Cat)
def describe(animal: Animal) -> str:
if is_cat(animal):
# Type checker knows animal is Cat here
return f"Cat {animal.name}, indoor={animal.indoor}"
else:
# Type checker knows animal is Dog here
return f"Dog {animal.name}, breed={animal.breed}"
animals: list[Animal] = [Cat("Whiskers", True), Dog("Rex", "Labrador")]
for a in animals:
print(describe(a))Full Example
# Compile: python3 type_narrowing.py
# Output:
# Cat Whiskers, indoor=True
# Dog Rex, breed=Labrador
from typing import TypeGuard, Union
from dataclasses import dataclass
@dataclass
class Cat:
name: str
indoor: bool
@dataclass
class Dog:
name: str
breed: str
Animal = Union[Cat, Dog]
def is_cat(animal: Animal) -> TypeGuard[Cat]:
return isinstance(animal, Cat)
def describe(animal: Animal) -> str:
if is_cat(animal):
return f"Cat {animal.name}, indoor={animal.indoor}"
else:
return f"Dog {animal.name}, breed={animal.breed}"
animals: list[Animal] = [Cat("Whiskers", True), Dog("Rex", "Labrador")]
for a in animals:
print(describe(a))
# isinstance narrowing (built-in, no TypeGuard needed)
def process(value: int | str | list) -> str:
if isinstance(value, int):
return f"int: {value * 2}"
elif isinstance(value, str):
return f"str: {value.upper()}"
else:
return f"list: {len(value)} items"
print(process(21))
print(process("hello"))
print(process([1, 2, 3]))
python3 type_narrowing.py