Type hints and static checking
Annotating functions and data, unions and generics, Protocol for structural typing, and how to run mypy and read its errors.
Annotating your code
Annotations are ordinary objects attached to the function; Python does not enforce them at runtime. Their value is that a checker can find type errors before the code runs, and that editors can complete names accurately.
from collections.abc import Iterable, Sequence, Mapping
def total(prices: Sequence[float], *, tax: float = 0.0) -> float:
return sum(prices) * (1 + tax)
def tag_counts(tags: Iterable[str]) -> dict[str, int]:
counts: dict[str, int] = {}
for tag in tags:
counts[tag] = counts.get(tag, 0) + 1
return counts
def first_name(user: Mapping[str, str]) -> str | None:
return user.get("name")
x: int = 0
rows: list[tuple[str, int]] = []
lookup: dict[str, list[str]] = {}
# annotate local containers when the inferred type would be too narrow
results: list[str] = []
print(total([1.5, 2.5], tax=0.2))| Hint | Meaning | Note |
|---|---|---|
Sequence[float] | Indexable, ordered, readable | Accepts list and tuple |
Iterable[str] | Anything you can loop over | The loosest useful input type |
dict[str, int] | Mapping from str to int | Built-in generics work from Python 3.9 |
str | None | A string or nothing | Use Optional[str] on older versions |
Callable[[int], str] | A function value | Describe parameters and return |
Any | Opt out of checking | Every use hides real errors downstream |
Unions, generics and Protocol
Narrow a union before using it: a checker understands isinstance, is None and truthiness tests and refines the type inside the branch.
from typing import Protocol, TypeVar
def render(value: int | str | None) -> str:
if value is None:
return "" # here the type is None
if isinstance(value, int):
return str(value) # here it is int
return value # here it is str
T = TypeVar("T")
def first_or(items: list[T], default: T) -> T:
return items[0] if items else default
class SupportsClose(Protocol): # structural: no inheritance needed
def close(self) -> None: ...
def shutdown(resource: SupportsClose) -> None:
resource.close()
shutdown(open("data.txt")) # a file object satisfies the protocol
from dataclasses import dataclass
@dataclass
class Config:
host: str
port: int = 8000
tags: list[str] = None # type: ignore[assignment]
# modern alternative for mutable defaults
from dataclasses import field
@dataclass
class Better:
tags: list[str] = field(default_factory=list)Protocol describes what an object can do rather than what it is, which keeps libraries decoupled from your class hierarchy. Mark it @runtime_checkable only if you need isinstance checks against it.- Prefer narrowing over
cast(); a cast silences the checker without proving anything. - Use
field(default_factory=...)for mutable defaults in dataclasses. - Reserve
Anyand# type: ignorefor boundaries you genuinely cannot type, and add a comment explaining why.
Running the checker
pip install mypy
mypy src/ # basic run
mypy --strict src/ # maximum checking
mypy --install-types # fetch stubs for third-party packages
python -m mypy --show-error-codes src/[tool.mypy]
python_version = "3.11"
warn_unused_ignores = true
warn_redundant_casts = true
disallow_untyped_defs = true
ignore_missing_imports = true
[[tool.mypy.overrides]]
module = "legacy.*"
ignore_errors = trueRead errors bottom-up within a file: a wrong argument type on line 80 is often caused by an inferred list[object] created on line 20. Add the annotation at the source and most later errors disappear at once.
error: Argument 1 has incompatible type— the call site disagrees with the annotation.error: Item "None" of "str | None" has no attribute— you skipped a None check.error: Need type annotation— an empty container; add the explicit hint.- Assigning a check to CI without fixing the backlog first makes the whole team ignore it. Adopt per-package, using the overrides table above.
FAQ
Do type hints slow my program down?
from __future__ import annotations, which turns every annotation into a string.mypy or pyright?
Related
Testing with pytest Object-oriented Python: classes and dunder methods
Last refreshed 2026-09-18.