Introduction
Python is dynamically typed, but type hints from PEP 484 have become a powerful tool for catching errors during development and improving readability. Static analyzers like mypy, pyright, and pyre report type mismatches before runtime.
This guide covers basic annotation syntax, collections.abc types, structural subtyping with Protocol, generic containers, and mypy configuration. Type hints are not enforced at runtime unless you add optional validation with pydantic or beartype.
Basic Type Hints
Annotate function parameters and return values: def greet(name: str) -> str:. Variable annotation is x: int = 0. Python 3.10+ uses built-in generic syntax list[str], dict[str, int]; older code imports List, Dict from typing.
Optional[str] and str | None (3.10+) express nullable types. Union[int, str] accepts multiple types. Any disables type checking; avoid it when possible.
from typing import Optional
def find_user(user_id: int) -> Optional[dict[str, str]]:
if user_id < 0:
return None
return {"id": str(user_id), "name": "Ali"}Protocol and Structural Subtyping
Protocol brings duck typing to static analysis: any class with the required methods satisfies the protocol without inheritance. This simplifies fakes in tests and loosely coupled design.
@runtime_checkable allows isinstance(obj, Readable) at runtime. Protocols are more flexible than ABCs; they provide type safety without wrapping third-party classes.
from typing import Protocol
class Readable(Protocol):
def read(self, n: int = -1) -> bytes: ...
def load(source: Readable) -> bytes:
return source.read()Generic and TypeVar
Generic classes and functions carry type parameters: class Stack[T]: ... TypeVar supports bounds: T = TypeVar('T', bound=Comparable). ParamSpec and TypeVarTuple (3.11+) express decorator and callback types more accurately.
mypy applies invariant container rules for generics: list[Dog] is not assignable to list[Animal]. Use Sequence[Animal] when covariance is needed. This distinction is the source of many collection API type errors.
- list[T], dict[K, V]: generic collections
- TypeVar: type parameter and bounds
- Protocol: structural interface
- TypedDict: dictionary shape definition
mypy Configuration
Define strictness in pyproject.toml or mypy.ini. disallow_untyped_defs requires types on all functions. ignore_missing_imports is a temporary fix for missing third-party stubs; prefer stubs packages like types-requests.
mypy --strict is the strictest mode; for gradual adoption of an existing codebase use per-module # mypy: disable-error-code or per-file overrides. Running mypy on every commit via pre-commit prevents regressions.
# pyproject.toml
[tool.mypy]
python_version = "3.12"
strict = true
warn_return_any = true
disallow_untyped_defs = trueTypedDict, Literal, and overload
TypedDict defines the shape of dicts with fixed keys; a lightweight alternative for API response models. Literal['GET', 'POST'] expresses fixed string value sets. @overload declares multiple signatures for the same function to static analysis.
Final and ClassVar mark immutability at class and module level. NewType creates branded types: UserId = NewType('UserId', int) prevents confusion with plain int.
from typing import TypedDict, Literal
class UserDict(TypedDict):
id: int
name: str
def request(method: Literal["GET", "POST"], url: str) -> bytes: ...Practical Integration
FastAPI and Pydantic validate types at runtime; mypy adds a second layer of safety. SQLAlchemy 2.0 Mapped[T] makes ORM models type-safe. A mypy pytest plugin can include type errors in the test suite.
Gradual adoption: type public APIs first, then spread to internal modules. Libraries marked py.typed improve mypy compatibility when types-* stubs are missing. Ruff ANN rules lint missing annotations.
- Start typing from the public API outward
- Add mypy to CI and pre-commit
- Use types-* stubs packages
- Simplify test doubles with Protocol
Conclusion
Type hints do not slow Python down; they improve IDE completion, refactor safety, and early error detection. Combined with mypy, they balance the flexibility of a dynamic language with the safety of a static one.
Advanced features like Protocol and Generic improve maintainability in large codebases. Making the type system a team standard shortens onboarding and reduces production bug rates.