Python type hints (introduced in Python 3.5 via PEP 484, massively expanded in subsequent versions) have moved from optional documentation aids to a core part of professional Python development. In 2025, typed Python is the default expectation in professional contexts. Here is how to use the type system effectively.
The Basics and What Has Changed
The fundamental purpose: type hints do not change Python’s runtime behaviour — Python remains dynamically typed. They are metadata that static type checkers (mypy, pyright, basedpyright), IDEs, and other tools use to catch errors before runtime. The basic syntax: `def greet(name: str) -> str: return f”Hello, {name}”`. Annotating variables: `x: int = 5`, `names: list[str] = []`. What has changed since the early days: PEP 585 (Python 3.9): you can now write `list[str]`, `dict[str, int]`, `tuple[int, …]` directly — you no longer need to import `List`, `Dict`, `Tuple` from `typing`. PEP 604 (Python 3.10): union types using `|` — `str | None` instead of `Optional[str]`, `int | str` instead of `Union[int, str]`. PEP 673 (Python 3.11): `Self` type — for methods returning the same type as their class. PEP 675 (Python 3.11): `LiteralString` — marks string parameters that must be literal strings (important for SQL injection prevention). Python 3.12: `type` statement for type aliases — `type Vector = list[float]` instead of `Vector = list[float]`. Python 3.13: `TypeIs` (narrower than `TypeGuard`). The `from __future__ import annotations` trick: in Python 3.7–3.9, this makes all annotations lazy (treated as strings), allowing forward references and circular types without quotes.
Key Patterns for Production Code
TypedDict (for dict-like structures): `from typing import TypedDict; class User(TypedDict): name: str; age: int` — creates a type-checkable dictionary type. More explicit than raw `dict[str, Any]`. Useful for API response types, config dictionaries, and JSON structures. Protocol (for structural typing / duck typing): `from typing import Protocol; class Readable(Protocol): def read(self) -> str: …` — any class implementing a `read() -> str` method satisfies `Readable`, without needing to explicitly inherit from it. This enables duck typing with type checking — the Go-like approach. Dataclasses vs NamedTuple vs TypedDict: use `@dataclass` when you need mutable objects with methods; use `NamedTuple` when you need tuple semantics with names (immutable, iterable, indexable); use `TypedDict` when you need to annotate dict structures. Generics: `from typing import TypeVar; T = TypeVar(‘T’); def first(items: list[T]) -> T: return items[0]` — in Python 3.12+, the simpler syntax is `def first[T](items: list[T]) -> T: return items[0]`. `Annotated`: `from typing import Annotated; UserId = Annotated[int, “must be positive”]` — metadata attached to types, used by frameworks like FastAPI (for request validation), Pydantic (for field constraints), and Beartype (runtime checking). `overload`: for functions with multiple valid signatures returning different types: `@overload; def process(x: str) -> str: …; @overload; def process(x: int) -> int: …; def process(x): …`
Type Checkers and Configuration
mypy: the original, most widely used type checker. Strict mode (`–strict`) enables all optional checks — the recommended setting for new projects. pyright (used by Pylance in VS Code): faster than mypy, with better generics inference. The recommended choice for VS Code. basedpyright: a fork of pyright with more features and stricter defaults — increasingly adopted as the best overall type checker. Pyrefly (Meta, 2025): a new checker from Meta written in Rust — extremely fast, designed for large codebases. Early adoption phase. Configuration for mypy (pyproject.toml):
“`toml
[tool.mypy]
strict = true
python_version = “3.12”
“`
Gradual typing strategy for existing codebases: start with `–ignore-missing-imports` to avoid failures from untyped third-party libraries; enable `–check-untyped-defs` early (flags untyped function bodies); enable strict incrementally as you annotate files. Per-file ignores: `# type: ignore[assignment]` suppresses a specific error type on that line. Typed Python in 2025: almost all major libraries now ship type stubs or inline types — numpy, pandas, requests, FastAPI, SQLAlchemy, Django (via django-stubs), Flask. The `py.typed` marker file indicates a package ships types. Libraries without types: use `pip install types-libraryname` to install community type stubs from typeshed.




