Python type hints are not just documentation. When used correctly with mypy, they catch bugs at “compile time” that would otherwise surface as runtime TypeErrors in production. The typing module is far more powerful than most developers realize — most stop at List[str] and miss the real power entirely.
⚡ TL;DR: Use
TypeVarwith bounds for generic functions,Protocolinstead of ABCs for structural typing,ParamSpecfor decorator typing, andTypeGuardfor narrowing. These four tools catch an entire class of runtime errors at development time.
TypeVar: Generic Functions Done Right
from typing import TypeVar, Generic, Sequence
T = TypeVar('T')
N = TypeVar('N', int, float) # Constrained TypeVar
Comparable = TypeVar('Comparable', bound='SupportsLessThan')
# Generic function — preserves type through transformation
def first(seq: Sequence[T]) -> T:
return seq[0]
first([1, 2, 3]) # int
first(['a', 'b']) # str — mypy knows the return type
# Constrained TypeVar
def add(a: N, b: N) -> N:
return a + b
add(1, 2) # OK — both int
add(1.0, 2.0) # OK — both float
add(1, 2.0) # mypy error — mixed types not allowed
# Generic class
class Stack(Generic[T]):
def __init__(self) -> None:
self._items: list[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
return self._items.pop()
stack = Stack[int]()
stack.push(1) # OK
stack.push('hello') # mypy error — str not allowed in Stack[int]
Protocol: Structural Typing Without Inheritance
from typing import Protocol, runtime_checkable
# Protocol defines a structural interface — any class that has these
# methods satisfies the protocol, no explicit inheritance needed
@runtime_checkable
class Drawable(Protocol):
def draw(self) -> None: ...
def resize(self, factor: float) -> None: ...
class Circle: # No Drawable inheritance!
def draw(self) -> None: print("Drawing circle")
def resize(self, factor: float) -> None: self.radius *= factor
class Square:
def draw(self) -> None: print("Drawing square")
def resize(self, factor: float) -> None: self.side *= factor
def render(shape: Drawable) -> None:
shape.draw()
render(Circle()) # OK — Circle satisfies Drawable structurally
render(Square()) # OK — Square satisfies Drawable structurally
render("hello") # mypy error — str does not satisfy Drawable
# Runtime check (due to @runtime_checkable)
print(isinstance(Circle(), Drawable)) # True
ParamSpec: Type Decorators Correctly
from typing import TypeVar, Callable, ParamSpec
import functools
P = ParamSpec('P')
R = TypeVar('R')
# Decorator that preserves function signature
def retry(times: int) -> Callable[[Callable[P, R]], Callable[P, R]]:
def decorator(func: Callable[P, R]) -> Callable[P, R]:
@functools.wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
for attempt in range(times):
try:
return func(*args, **kwargs)
except Exception:
if attempt == times - 1: raise
return wrapper
return decorator
@retry(times=3)
def fetch_user(user_id: int, timeout: float = 5.0) -> dict:
return {}
# mypy knows fetch_user takes (int, float=5.0) -> dict
fetch_user(123) # OK
fetch_user(123, 10.0) # OK
fetch_user("wrong") # mypy error — str not int
TypeGuard: Narrowing in Custom Validators
from typing import TypeGuard, Union
# Without TypeGuard — mypy can't narrow the type
def is_str_list(val: list[object]) -> bool:
return all(isinstance(x, str) for x in val)
def process(val: list[object]) -> None:
if is_str_list(val):
val[0].upper() # mypy error — still list[object]
# With TypeGuard — mypy narrows type in if branch
def is_str_list_tg(val: list[object]) -> TypeGuard[list[str]]:
return all(isinstance(x, str) for x in val)
def process_typed(val: list[object]) -> None:
if is_str_list_tg(val):
val[0].upper() # OK — mypy knows val is list[str] here
Python Typing Cheat Sheet
- ✅
TypeVar('T')for generic functions that preserve input type - ✅
TypeVar('N', int, float)for constrained generics - ✅
Protocolinstead of ABC for structural typing - ✅
ParamSpecto type decorators that preserve function signatures - ✅
TypeGuard[T]for custom type narrowing functions - ✅ Run mypy in CI — catches bugs tests miss
- ❌ Don’t annotate everything — focus on public API surfaces
- ❌ Don’t use
Any— it disables type checking entirely
Python typing pairs naturally with Python dataclasses — @dataclass generates type-safe __init__ methods automatically. For runtime performance of typed code, see how __slots__ optimization reduces the memory overhead of heavily-typed class hierarchies. External reference: mypy generics documentation.
Master Python type hints and static analysis
→ View Course on Udemy — Hands-on video course covering every concept in this post and more.
Sponsored link. We may earn a commission at no extra cost to you.
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.
