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. This guide focuses on Python typing generics, with the production trade-offs senior engineers need to make sound decisions.
⚡ 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)) # TrueParamSpec: 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 intTypeGuard: 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] herePython 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.
Free Weekly Newsletter
🚀 Don’t Miss the Next Cheat Code
You just read something most developers never learn. Get more secrets like this delivered every week — JavaScript internals, Python optimizations, AWS architectures, system design, and AI workflows.
Join 1,000+ senior developers who actually level up. Zero fluff, pure signal.
Related: Unit Testing Best Practices: Write Tests That Actually Catch Bugs Before Production · AI PR Reviewers: Diff-Scoped Context Windows That Catch Real Bugs · Node Module Compile Cache: Faster Cold Starts for Heavy CLIs
Watch: 100 Days of AI on YouTube
Short videos from CheatCoders, one AI engineering topic per day.
Most viewed
- Python Decorators Explained: From Simple Wrappers to Production Patterns
- Claude Code vs Cursor vs GitHub Copilot: Choose by Workflow, and Set Up One Repo That Works With All Three
- AI Agent Frameworks in 2025: LangGraph vs CrewAI vs AutoGen vs Raw API
- Python String Methods: Every str Method With Real Production Examples
- REST API Design Best Practices: The Patterns That Make APIs a Joy to Use
Newly added
- AWS Systems Manager Parameter Store: Hierarchical Runtime Config for Coding Agents
- Amazon Cognito Identity Pools: Federate Coding-Agent Tool Callers Without Long-Lived Keys
- Amazon Athena for Coding Agents: Query Tool Audit Logs and Eval Traces on S3 Without a Warehouse
- AWS Batch for Coding Agents: Parallel Overnight PR Reviews and Long-Running Jobs Without Lambda Timeouts
- Amazon Bedrock Prompt Management: Version and A/B Test Coding-Agent System Prompts
Deep-dive PDF
Get the expanded guide for this post — extra diagrams-style checklists, failure modes, and a production walkthrough. Free when you subscribe to CheatCoders.
Already subscribed? or open the subscribe page.
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.
