Python Type Hints

Python is dynamically typed, but since Python 3.5 you can annotate variables, parameters, and return values with types. The interpreter ignores these annotations at runtime; type checkers such as mypy and pyright read them to find bugs before your code runs: None where a string was expected, a misspelled attribute, a function called with the wrong arguments.

Type hints are now standard in serious Python codebases. They power editor autocompletion and refactoring, and they drive runtime tools like Pydantic, FastAPI, and dataclasses. This page covers the syntax you'll use daily and the features that make typing Python pleasant rather than painful.

TL;DR

Quick Example

mypy or pyright will reject User(id="1", email="a@b.c"), display_name(None), a status="banned", and any notifier object lacking a compatible send method, all without running the code.

Core Concepts

Basic Annotations

Use X | Y for unions and X | None for optional values. Optional[X] and Union[X, Y] from typing are the older spellings. Any opts out of checking; object means "anything, but you must narrow before using it", which is the safer choice.

Generics

Generic functions and classes work over a type parameter:

Bounds ([T: Number]) and constraints restrict what T can be. Prefer abstract parameter types (Sequence[T], Mapping[K, V], Iterable[T] from collections.abc) over concrete list and dict for inputs, so callers can pass any compatible collection.

Protocols: Structural Typing

A Protocol describes the shape an object needs, not its class hierarchy. Any object with matching methods and attributes satisfies it, with no inheritance required:

This is Python's duck typing made checkable, the same idea as Go interfaces.

TypedDict, Literal, and Friends

Narrowing

Checkers track what a value can be in each branch:

Write your own narrowing functions with TypeIs (3.13) or TypeGuard, and use assert_never in the final else of exhaustive matches so adding a new variant becomes a type error.

Type Checkers

Enable them gradually: start with default settings, then turn on strict (mypy) or "typeCheckingMode": "strict" (pyright) for new modules. Third-party libraries ship types inline (py.typed) or through types-* stub packages.

Best Practices

Annotate Boundaries First

Public functions, class attributes, and module interfaces give the biggest return. Local variables are usually inferred, so annotating x: int = 5 adds noise without value.

Accept Abstract, Return Concrete

Take Iterable[str] or Mapping[str, int] as parameters so callers have flexibility; return list[str] or dict[str, int] so callers know exactly what they get.

Avoid Any Leaks

Any spreads silently: anything derived from it is unchecked too. Prefer object, a Protocol, or a TypedDict. Turn on disallow_any_generics and warn_return_any as you tighten up.

Validate at Runtime Where Data Enters

Type hints don't validate input. For request bodies, config, and API responses, use a runtime validator such as Pydantic, msgspec, or attrs with validators, which reads the same annotations. The TypeScript world does the same with Zod.

Common Mistakes

Mutable Defaults Hidden Behind Types

Ignoring None in Return Types

Silencing Errors With # type: ignore Everywhere

Blanket ignores hide real bugs. Use targeted codes (# type: ignore[attr-defined]), enable warn_unused_ignores, and fix the underlying type where you can.

FAQ

Do type hints make Python faster?

No. CPython ignores annotations at runtime. Tools like mypyc and Cython can compile typed code for speed, but ordinary hints are for correctness and tooling, not performance.

mypy or pyright?

Both are excellent. Pyright is faster and is what VS Code's Pylance uses, so running it in CI keeps editor and CI diagnostics aligned. mypy has the longest history and plugins for frameworks like Django. Pick one, run it in CI, and configure your editor to match.

Should I use List[int] or list[int]?

list[int]. Built-in generics work from Python 3.9, and X | Y from 3.10. The capitalized typing.List, Dict, and Optional are legacy aliases kept for compatibility.

How do I type a decorator?

Use ParamSpec and a TypeVar for the return type so the decorated function keeps its signature: def deco[**P, R](fn: Callable[P, R]) -> Callable[P, R]. See Python decorators.

Related Topics

References