How it works
The explanation, with examples you can run.
Stored, not enforced
def double(x: int) -> int: return x * 2 print(double("ab")) # abab: nothing checks the hint at runtimeprint(double.__annotations__) # {'x': <class 'int'>, 'return': <class 'int'>}The call succeeds because str * int is valid. Run mypy (2.4 here) on a file containing that call and it reports, without running anything:
error: Argument 1 to "double" has incompatible type "str"; expected "int" [arg-type]That split is deliberate (PEP 484). Hints cost nothing at runtime, they can be added gradually to an existing code base, and the checker can reason about things no runtime check could, such as every path through a function or None being possible.
What a static checker gives you
A checker infers types through your program and complains when they don't line up: passing a str where an int is expected, forgetting that a dict lookup with .get() may return None, calling a method that doesn't exist, or returning the wrong type from one branch. The useful vocabulary is small: X | None for optional values (3.10+), list[str] and dict[str, int] for containers, Callable, TypedDict for JSON-shaped dicts, and Protocol for "anything with these methods". reveal_type(x) asks the checker what it thinks x is.
Python 3.12 added cleaner generics (PEP 695):
def first[T](items: list[T]) -> T: # 3.12+ syntax (PEP 695) return items[0] type Pair[T] = tuple[T, T] # 3.12+ type alias statementAnnotations at runtime
Some tools treat annotations as data. @dataclass reads the class annotations to decide which fields go in __init__, though it still doesn't check the values (Point("a") happily stores a string in x: int). Pydantic and FastAPI do validate: they read the hints with typing.get_type_hints() and convert or reject incoming data. isinstance works with plain classes, and with a Protocol marked @runtime_checkable, but it only checks that the methods exist by name:
from typing import Protocol, runtime_checkable @runtime_checkableclass Closeable(Protocol): def close(self) -> None: ... class Door: def close(self): return "slam" print(isinstance(Door(), Closeable)) # True: only checks that a close attribute existsParameterised generics like list[int] can't be used with isinstance at all; Python raises TypeError: isinstance() argument 2 cannot be a parameterized generic.
When annotations are evaluated
Up to 3.13, annotations are evaluated when the def or class statement runs, so a hint that names a class defined later in the file needs quotes ("Node") or from __future__ import annotations, which turns every annotation into a string. Python 3.14 implements PEP 649: annotations are evaluated lazily, the first time something asks for them, so forward references just work. Code that reads annotations should use typing.get_type_hints() or, on 3.14, annotationlib.get_annotations() rather than poking at __annotations__ directly.
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.