Type hints stopped being optional for serious Python around 2020. Every major framework ships with them (FastAPI, Pydantic, SQLAlchemy 2.0, Django 4+), every decent CI pipeline runs a type checker, and every library worth using publishes py.typed stubs. If you're still writing untyped Python, you're leaving bugs on the table that a five-second pyright run would catch.
The core system is straightforward: you annotate function parameters and return types, and a static checker (pyright, mypy) verifies consistency without ever running your code. Python 3.12 overhauled the syntax with PEP 695, making generics and type aliases dramatically cleaner. The old TypeVar / TypeAlias ceremony is still everywhere in existing code, so you need to know both — but new code should use the modern syntax.
This page covers everything from basic annotations to advanced patterns like TypedDict for API responses, Protocol for structural subtyping, type guards for narrowing, and the @overload decorator for functions with multiple signatures.
Master python type hints
Take the Python Architecture course with hands-on lessons and challenges.
The pipe syntax (3.10+) replaces Union and Optional. Use `X | None` instead of `Optional[X]` — it reads better and requires no typing import. The type checker narrows the union after a None check.
PEP 695 eliminates the TypeVar and Generic boilerplate. Type parameters go in brackets after the function or class name. Bounded parameters use `T: Bound`. The `type` soft keyword creates type aliases that support forward references natively.
TypedDict defines dictionaries with specific keys and value types. Use Required/NotRequired (3.11+) for mixed optionality. Combine with Literal for string enums. This is the standard pattern for typing JSON payloads from REST APIs.
Protocol enables structural subtyping: any class with a matching `render()` method satisfies Renderable, no inheritance needed. The @overload decorator tells the type checker that different input types produce different return types — the implementation signature is not visible to callers.
TypeGuard narrows a type in the True branch of a conditional. TypeIs (3.13+) is stricter but narrows in both branches. For simple isinstance checks, the type checker narrows automatically. Pattern matching (match/case) also narrows types.
You'll encounter TypeVar everywhere in libraries, typeshed stubs, and pre-3.12 codebases. Know the migration path: `T = TypeVar('T')` becomes `[T]` in the function/class signature, `TypeAlias` becomes the `type` statement, and `Generic[T]` is no longer needed as a base class.
Using `dict[str, Any]` as a catch-all for structured data — this kills type safety for every downstream access
Use TypedDict for dictionaries with known keys: `class Config(TypedDict): host: str; port: int`. The type checker validates every key access. Reserve `dict[str, Any]` for genuinely dynamic data.
Writing `Optional[X]` or `Union[X, None]` instead of `X | None` — the old syntax is verbose and requires imports
Use the pipe syntax on Python 3.10+: `def find(id: int) -> User | None`. It reads like English and requires no `from typing import` anything. Only fall back to `Optional` if you support 3.9.
Assuming type hints are enforced at runtime — then being confused when `greet(42)` doesn't raise an error despite `name: str`
Type hints are metadata only. Python never checks them at runtime. You must run a static checker (pyright, mypy) in your CI pipeline or editor. For runtime validation, use Pydantic or beartype.
Creating `T = TypeVar('T')` at module scope in new Python 3.12+ code instead of using the PEP 695 syntax
Use `def f[T](x: T) -> T` and `class Box[T]` in 3.12+ code. The PEP 695 syntax is scoped (no module-level pollution), more readable, and what the language is converging on. Reserve old-style TypeVar for libraries that still support 3.11.
Python type hints are static annotations checked by tools like pyright and mypy, with zero runtime overhead. Python 3.12 introduced PEP 695, which replaces TypeVar boilerplate with clean `def f[T]()` syntax and the `type` statement for aliases. For structured data, use TypedDict over bare dicts. For interfaces, use Protocol for structural subtyping. Combine Literal types, type guards, and @overload to model complex function signatures precisely. Run a type checker in CI — type hints without enforcement are just comments.
Interactive lessons and challenges on Stanza, practice in VS Code, Cursor, or the web.
Interactive lessons and challenges, right in your code editor.
Check the free courses. No credit card.