Type Narrowing
The quality of real-world typing often depends more on narrowing than on declarations. If you cannot safely narrow broad input types after runtime checks, codebases tend to drift toward casts and `Any`.
Quick takeaway: narrowing is how runtime checks become static type information. `isinstance`, `match`, `TypeGuard`, and `TypeIs` let you replace many unsafe casts with explicit, checkable flow.
Narrowing Picture
TypeGuard vs TypeIs
from typing import TypeGuard, TypeIs
def is_str_list(values: list[object]) -> TypeGuard[list[str]]:
return all(isinstance(value, str) for value in values)
def is_int(value: int | str) -> TypeIs[int]:
return isinstance(value, int)`TypeGuard` is good when a complex structure should be treated as a more specific type. `TypeIs` is better for true type predicates that behave more like `isinstance` checks.
match Helps Too
def describe(value: int | str | None) -> str:
match value:
case int():
return "int"
case str():
return "str"
case None:
return "none"Truthiness Narrowing Has Limits
if value:collapses empty strings, zero, empty collections, andNone.- If your design must distinguish "missing" from "empty," truthiness alone is too blunt.
Practical Connections
- input validation helpers
- discriminator-based branching
- API payload parsing
Checklist
Prefer narrowing over casting
If possible, make runtime checks visible to the type checker instead of scattering `cast()` calls.
Do not overuse truthiness
Explicit comparisons are safer when empty and missing values mean different things.
Choose `TypeGuard` vs `TypeIs` carefully
`TypeGuard` fits structural refinement; `TypeIs` fits true type predicates.
Use discriminators
Tagged unions with explicit `kind` or `type` fields make both runtime logic and narrowing much cleaner.