Skip to main content

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 Protocol and explain why they are superior to ABCs for library boundaries
  • Preserve callable signatures through decorators using ParamSpec and Concatenate
  • Build recursive types, self-referential generics, and generic protocols for complex data structures
  • Use @overload to give a single function multiple precise signatures
  • Apply TypeGuard, TypeIs, and assert_never for 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.typed markers 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:

  • TypeVar for generic functions and classes
  • Generic[T] base class and multi-parameter generics
  • Bound vs constrained type variables
  • Covariance (Sequence[Dog] is a Sequence[Animal])
  • Contravariance (why Callable[[Animal], None] accepts Callable[[Dog], None])
  • Invariance and why list must 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:

  • Protocol classes and structural subtyping (PEP 544)
  • Protocol vs ABC: when to use which
  • runtime_checkable for isinstance support (and its limitations)
  • Composing protocols for complex interfaces
  • Nominal vs structural subtyping and the tradeoffs
  • How FastAPI's Depends and 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:

  • ParamSpec and P.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:

  • Self type for fluent interfaces and builder patterns
  • TypeVarTuple and Unpack for 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:

  • @overload decorator and implementation functions
  • When overload is correct vs when generics are better
  • TypeGuard for custom type narrowing predicates
  • TypeIs (PEP 742) for bidirectional narrowing
  • assert_never and exhaustiveness checking with unions
  • match statement 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 annotations
  • isinstance with typing constructs (and why most do not work)
  • beartype for zero-overhead runtime checking
  • typeguard for 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.toml configuration 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 (.pyi files)
  • The py.typed marker and PEP 561
  • CI integration: pre-commit hooks, GitHub Actions, blocking merges
  • Migrating an untyped codebase to full type coverage

The 2 Projects

#ProjectCore Skills
01Type-Safe Event SystemGenerics, Protocol, TypeVar, pub/sub pattern, compile-time event type safety
02Typed Configuration LibraryRuntime 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. TypeVarTuple and projects like jaxtyping encode 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
  • TypeVar enables generic abstraction; variance determines assignability rules for generic containers
  • Protocol brings static verification to duck typing without requiring inheritance
  • ParamSpec is the only correct way to type decorators that preserve signatures
  • @overload and TypeGuard give 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
© 2026 EngineersOfAI. All rights reserved.