Module 02: Advanced Type System
Reading time: ~10 minutes | Level: Advanced | 7 Topics + 2 Projects
Here is a decorator that most Python engineers write at some point:
from functools import wraps
from typing import Callable, TypeVar
T = TypeVar("T")
def retry(func: Callable[..., T]) -> Callable[..., T]:
@wraps(func)
def wrapper(*args, **kwargs) -> T:
for attempt in range(3):
try:
return func(*args, **kwargs)
except Exception:
if attempt == 2:
raise
raise RuntimeError("unreachable")
return wrapper
It looks typed. It passes mypy. But it is lying.
Call retry(some_function) and hover over the result in your editor. The signature is (*args: Any, **kwargs: Any) -> T. Every parameter name, every type constraint, every default value from the original function -- gone. Your IDE cannot autocomplete. Your type checker cannot catch wrong arguments. The type annotation is theater.
The correct version uses ParamSpec:
from typing import Callable, TypeVar, ParamSpec
P = ParamSpec("P")
T = TypeVar("T")
def retry(func: Callable[P, T]) -> Callable[P, T]:
@wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
for attempt in range(3):
try:
return func(*args, **kwargs)
except Exception:
if attempt == 2:
raise
raise RuntimeError("unreachable")
return wrapper
Now the wrapper preserves the exact signature of the decorated function. Your editor knows every parameter. Your type checker catches every mistake. The type annotation is infrastructure.
This module teaches you the difference between type annotations that exist and type annotations that work.
Why Advanced Typing Matters Now
Python's type system has evolved dramatically since PEP 484 introduced type hints in Python 3.5. What started as optional annotations for documentation has become a full structural type system capable of expressing complex invariants that static analyzers enforce at zero runtime cost.
The ecosystem has caught up:
- FastAPI derives request validation, serialization, and OpenAPI schemas entirely from type hints
- Pydantic v2 compiles type hints into Rust-powered validators
- SQLAlchemy 2.0 uses
Mapped[T]generics for column types - PyTorch type stubs enable autocompletion across thousands of tensor operations
- mypy and pyright now run in CI pipelines at Google, Meta, Dropbox, and Stripe
If you are writing production Python in 2025 and your type annotations are limited to str, int, list[str], and Optional[X], you are using roughly 10% of the system. This module covers the other 90%.
:::danger The Cost of Shallow Typing Shallow type annotations create a false sense of safety. When your decorator erases signatures, when your generic container loses its element type, when your Protocol does not capture the right structural contract -- bugs slip through the exact layer that was supposed to catch them. Incomplete typing is worse than no typing because it erodes trust in the tool. :::
What Mastery of This Module Looks Like
By the end of all 7 topics you will be able to:
- Write generic classes and functions with
TypeVar, including bound and constrained type variables - Explain covariance, contravariance, and invariance and choose the correct variance for any generic container
- Define structural interfaces with
Protocoland explain why they are superior to ABCs for library boundaries - Preserve callable signatures through decorators using
ParamSpecandConcatenate - Build recursive types, self-referential generics, and generic protocols for complex data structures
- Use
@overloadto give a single function multiple precise signatures - Apply
TypeGuard,TypeIs, andassert_neverfor exhaustive type narrowing - Integrate runtime type checking with
beartype,typeguard, and Pydantic - Configure mypy and pyright for strict mode, gradual adoption, and CI enforcement
- Ship typed libraries with
py.typedmarkers and type stubs
The Architecture of Python's Type System
Before diving into individual topics, it helps to see how the pieces fit together:
The system has two enforcement paths: static analysis (compile-time, zero runtime cost) and runtime checking (validation at boundaries). Production systems use both. This module covers the full stack.
The 7 Topics
01 -- Generics and TypeVar
The foundation of type-safe abstraction
When you write a function that works on "any type" but returns "that same type," you need generics. Without TypeVar, you are forced to choose between Any (no safety) and concrete types (no reuse). Generics let you express "this function accepts a T and returns a T" -- and the type checker enforces it.
But generics go deeper than most tutorials show. Bound TypeVars constrain T to subclasses of a specific type. Constrained TypeVars restrict T to an explicit set. And variance -- covariance, contravariance, invariance -- determines whether list[Dog] is assignable to list[Animal]. (It is not, and the reason reveals a fundamental truth about mutable containers.)
You will learn:
TypeVarfor generic functions and classesGeneric[T]base class and multi-parameter generics- Bound vs constrained type variables
- Covariance (
Sequence[Dog]is aSequence[Animal]) - Contravariance (why
Callable[[Animal], None]acceptsCallable[[Dog], None]) - Invariance and why
listmust be invariant - Real patterns from SQLAlchemy, FastAPI, and PyTorch
02 -- Protocol and Structural Subtyping
Duck typing, made rigorous
Python's power comes from duck typing -- if it has a .read() method, it is file-like. But duck typing has no static verification. You pass an object that is almost right, and it fails at runtime on the one method you forgot.
typing.Protocol solves this. It defines structural interfaces that type checkers enforce without requiring inheritance. Your class does not need to know about the Protocol. It just needs to have the right methods with the right signatures.
You will learn:
Protocolclasses and structural subtyping (PEP 544)- Protocol vs ABC: when to use which
runtime_checkableforisinstancesupport (and its limitations)- Composing protocols for complex interfaces
- Nominal vs structural subtyping and the tradeoffs
- How FastAPI's
Dependsand pytest fixtures use structural patterns - Designing library boundaries with protocols instead of base classes
03 -- ParamSpec and Concatenate
Typing decorators that actually preserve signatures
This is the topic that opened this overview. Before ParamSpec (PEP 612), there was no way to express "this decorator returns a function with the same signature as its input." Every decorator erased parameter information. Every typed wrapper was a lie.
ParamSpec captures the full parameter specification of a callable -- names, types, defaults, keyword-only markers. Concatenate lets you prepend parameters to a captured spec. Together, they make decorator typing correct for the first time.
You will learn:
ParamSpecandP.args/P.kwargs- Why
Callable[..., T]is insufficient for decorators Concatenate[X, P]for decorators that add parameters- Typing context managers, retry wrappers, authentication decorators
- Stacking multiple typed decorators without losing information
- The patterns used by FastAPI's dependency injection system
04 -- Advanced Generic Patterns
The type-level programming that powers frameworks
Once you understand basic generics, a world of advanced patterns opens. Self type (PEP 673) lets methods return the correct subclass type. TypeVarTuple (PEP 646) enables variadic generics for tensor shape typing. Recursive types model tree structures. Generic protocols combine structural subtyping with parameterized types.
These are the patterns that framework authors use. Understanding them turns you from a framework consumer into someone who can read, extend, and build frameworks.
You will learn:
Selftype for fluent interfaces and builder patternsTypeVarTupleandUnpackfor variadic generics- Recursive type aliases for trees, JSON, and nested structures
- Generic protocols -- protocols parameterized by type variables
- Multi-TypeVar generics for key-value containers, Result types, and Either patterns
- How PyTorch uses shape typing and how Pydantic uses generic models
05 -- Overload and Type Narrowing
Multiple signatures, exhaustive checking
Some functions genuinely return different types depending on their inputs. json.loads returns Any, but you know it returns a dict when you pass a JSON object string. @overload lets you declare multiple signatures so the type checker picks the right return type based on the arguments.
Type narrowing goes the other direction -- starting from a union type and proving which branch you are in. TypeGuard and TypeIs let you write custom type predicates. assert_never ensures your match or if/elif chain handles every case, and the type checker proves it at compile time.
You will learn:
@overloaddecorator and implementation functions- When overload is correct vs when generics are better
TypeGuardfor custom type narrowing predicatesTypeIs(PEP 742) for bidirectional narrowingassert_neverand exhaustiveness checking with unionsmatchstatement integration with type narrowing- Patterns from standard library:
open(),json.loads(),subprocess.run()
06 -- Runtime Type Checking
Validation at system boundaries
Static analysis catches bugs before execution. But it cannot validate data that arrives from outside your program -- HTTP requests, config files, database rows, user input. At system boundaries, you need runtime type checking.
This topic covers the spectrum from lightweight assertion (beartype) to full validation frameworks (Pydantic). You will understand when to use each, how they interact with static analysis, and how to avoid the trap of checking types everywhere instead of only at boundaries.
You will learn:
typing.get_type_hints()for runtime introspection of annotationsisinstancewith typing constructs (and why most do not work)beartypefor zero-overhead runtime checkingtypeguardfor comprehensive runtime validation- Pydantic v2's model validation architecture
- The boundary principle: validate at edges, trust internally
- Performance implications of runtime checking
07 -- Static Analysis in Practice
Making the type checker your first line of defense
Knowing the typing constructs is necessary but not sufficient. You also need to configure your tools correctly, integrate them into your workflow, and adopt them incrementally in existing codebases.
This topic is the engineering chapter. It covers mypy and pyright configuration, strict mode, per-module overrides, gradual typing strategies, type stubs for untyped dependencies, the py.typed marker for library authors, and CI integration that blocks type-unsafe merges.
You will learn:
mypy.ini/pyproject.tomlconfiguration in depth- pyright and pylance settings for VS Code
- Strict mode vs gradual mode and when to use each
- Per-module type checking overrides
- Writing and distributing type stubs (
.pyifiles) - The
py.typedmarker and PEP 561 - CI integration: pre-commit hooks, GitHub Actions, blocking merges
- Migrating an untyped codebase to full type coverage
The 2 Projects
| # | Project | Core Skills |
|---|---|---|
| 01 | Type-Safe Event System | Generics, Protocol, TypeVar, pub/sub pattern, compile-time event type safety |
| 02 | Typed Configuration Library | Runtime validation, type introspection, Pydantic patterns, generic config containers |
Project 01 -- Type-Safe Event System
Build a fully typed publish-subscribe system where event types are enforced at compile time. Subscribing a handler with the wrong signature is a type error. Publishing an event with the wrong payload is a type error. The system uses Generic, Protocol, ParamSpec, and @overload to achieve full type safety with zero runtime overhead.
Project 02 -- Typed Configuration Library
Build a configuration library that reads from environment variables, YAML files, and dictionaries, validates values against type annotations at runtime, and provides fully typed access to configuration values. Uses get_type_hints, generic containers, Pydantic-style validation, and the boundary principle to validate once at load time and provide safe typed access everywhere else.
Prerequisites for This Module
- Module 01: Metaprogramming -- metaclasses, descriptors,
__init_subclass__(understanding Python's class machinery) - Intermediate OOP -- ABCs, dataclasses, inheritance, SOLID principles
- Basic type hints --
int,str,list[str],Optional,Union,dict[str, Any] - Decorator fundamentals -- writing and applying decorators,
functools.wraps
:::note On Ordering This module follows Metaprogramming deliberately. Metaclasses teach you how Python constructs types at runtime. This module teaches you how to describe and constrain types at analysis time. Together, they give you complete mastery over Python's type machinery -- both the runtime reality and the static description. :::
Why This Module Matters for AI/ML Engineering
Type safety is not a luxury in ML systems. It is infrastructure:
- Tensor shape errors are the most common bug in deep learning code.
TypeVarTupleand projects likejaxtypingencode shapes in the type system - Pipeline composition in scikit-learn, Airflow, and Prefect relies on typed interfaces to ensure stage compatibility
- API contracts in FastAPI, LangChain, and LlamaIndex use Pydantic models derived from type annotations
- Configuration management for training runs (learning rate, batch size, model architecture) benefits from typed config objects that catch errors before a 72-hour training run starts
- Plugin systems in ML frameworks use Protocol-based interfaces for extensibility without tight coupling
Every hour spent learning advanced typing saves ten hours of debugging shape mismatches, config typos, and interface violations in production ML systems.
How to Use This Module
The topics build on each other linearly. Generics are required for Protocols. Protocols are used in ParamSpec examples. Advanced patterns combine everything from the first three topics. Overload and narrowing add precision. Runtime checking and static analysis are the enforcement layer.
Each topic contains:
- An opening puzzle that reveals why the naive approach fails
- Complete mental models with type-level diagrams
- Runnable code verified against mypy strict mode and pyright
- Common mistakes from real production codebases
- An AI/ML engineering connection
- Practice challenges with type-checking verification
:::tip Verify Everything
Every code example in this module should be checked with a type checker. Do not just run the code -- run mypy --strict on it. The point of this module is not that the code executes correctly (it will), but that the type checker can verify its correctness without executing it.
:::
Key Takeaways for This Overview
- Python's type system is a full structural type system, not just annotations for documentation
TypeVarenables generic abstraction; variance determines assignability rules for generic containersProtocolbrings static verification to duck typing without requiring inheritanceParamSpecis the only correct way to type decorators that preserve signatures@overloadandTypeGuardgive the type checker precise information about conditional return types- Runtime checking belongs at system boundaries; static checking belongs everywhere else
- mypy and pyright in strict mode, integrated into CI, are the production standard
- Advanced typing is not academic -- it is the foundation of FastAPI, Pydantic, SQLAlchemy 2.0, and modern ML frameworks
