Type hints are design-time contracts for readers, editors, and static analyzers. At runtime, Python ignores them — def double(x: int) -> int: return x * 2 happily accepts double("ha") and returns "haha". The hints guide tools, not the interpreter.
Think of type hints like blueprint annotations. An architect writes dimensions on a plan, but the construction crew does not measure every wall against the blueprint at build time — they build, and the blueprint catches mistakes before anything is built. Static type checkers (mypy, pyright) are the inspectors who read the plan and flag problems before your code runs.
Annotate public boundaries, accept abstract protocols where callers may provide many concrete implementations, and return concrete types when the caller needs predictable operations.
# [OLDER / 3.9, CURRENT - 3.10-3.14] Works on Python 3.9+ [PEP 585]from collections.abc import Iterable, Mappingdef render(headers: Mapping[str, str], rows: Iterable[str]) -> list[str]: prefix = headers.get("prefix", "") return [prefix + row for row in rows]At runtime, ordinary Python execution still happens. The interpreter does not reject a wrong type just because a hint exists.
# [CURRENT - 3.10-3.14] Works on Python 3.xdef double(x: int) -> int: return x * 2print(double("ha")) # 'haha'Function annotations are exposed through __annotations__. Class annotations are visible to tools and to libraries such as dataclasses, but Python itself does not automatically validate them. In Python 3.14+, annotations are evaluated lazily; use typing.get_type_hints() when runtime code needs resolved annotation values.
# [CURRENT - 3.10-3.14] Works on Python 3.xdef connect(host: str, port: int = 5432) -> tuple[str, int]: return host, portprint(connect.__annotations__)Any and object are not interchangeable. Any disables static checking for that value. object accepts any runtime object but forces explicit narrowing before type-specific operations.
# [CURRENT - 3.10-3.14] Works on Python 3.xfrom typing import Anydef unsafe(value: Any): return value.made_up_method()def safer(value: object): if isinstance(value, str): return value.casefold() return repr(value)Parameterized built-in generics such as list[int] carry annotation meaning only — isinstance rejects them at runtime.
# [OLDER / 3.9, CURRENT - 3.10-3.14] Works on Python 3.9+ [PEP 585]try: isinstance([1, 2, 3], list[int])except TypeError as exc: print(exc)Built-in generic aliases like list[str] are Python 3.9+ PEP 585. The union operator T | U is Python 3.10+ PEP 604. Current project guidance targets Python 3.10-3.14. Python 3.9 and below are End-of-Life, so modern syntax is the baseline unless you are explicitly maintaining an older fleet.
# [CURRENT - 3.10-3.14] Requires Python 3.10+ [PEP 604]def find_user(name: str) -> dict[str, str] | None: if not name: return None return {"name": name}Do not confuse "optional value" with "optional parameter." str | None means the value may be None. The parameter becomes optional only if it has a default.
# [CURRENT - 3.10-3.14] Requires Python 3.10+ [PEP 604]def parse_limit(raw: str | None = None) -> int: if raw is None: return 100 return int(raw)If code needs evaluated annotations at runtime, use typing.get_type_hints() instead of assuming raw __annotations__ already contain the final objects.
Type hints are not input validation, security policy, or data cleansing. Validate untrusted data explicitly at the boundary and use hints to improve design and static feedback.
Use Protocol when behavior matters more than inheritance, and abstract collection ABCs when you want to be liberal in what you accept.
# [OLDER / 3.9, CURRENT - 3.10-3.14] Works on Python 3.9+ [PEP 585]from typing import Protocolclass Closable(Protocol): def close(self) -> None: ...def close_all(resources: list[Closable]) -> None: for resource in resources: resource.close()Use concrete return types when the caller needs stable semantics. When annotations should also drive generated record behavior, pair them with NamedTuple or dataclasses; see .