Basic annotations
corebeginnerA type annotation writes the expected type after a colon — name: type for a variable, param: type for an argument, -> type for a return value. Python stores it but never checks it while running.
Think of it as
An annotation is a label on a shipping box, not a lock on it — the label says "books" so the warehouse (and a type checker) knows what to expect, but nothing stops you physically putting shoes inside. def greet(name: str) -> str: tells a reader and a type checker the shape, while Python itself accepts whatever you pass.
What we're doing: Annotate a variable and a function's parameters and return value, then show the annotations do not stop the function running with the "wrong" types.
- 1
- age: int = 5 is a variable annotation — int documents the expected type, the = 5 still does the actual assigning.
- 3
- name: str and times: int annotate parameters; -> str annotates what the function returns.
Ana Ana
5Why this works: Annotations are metadata Python stores (in __annotations__) and otherwise ignores at runtime — greet("Ana", 2) runs exactly the same whether or not the annotations are present. Their value comes from a separate tool (a type checker) or a human reader, not from the interpreter.
Believing an annotation prevents passing the wrong type
Wrong
Better
What you see: A caller expects TypeError: expected str for the annotation itself, but any error that happens comes from the code's own logic, not from type checking.
Why: Python never inspects a parameter annotation before calling the function. Whatever failure happens is a consequence of the function body running with the actual value it was given — a type checker, run separately, is what would have flagged greet(123) before the code ever ran.
- x: int — variable annotation
- def f(x: int) — parameter annotation
- -> str: — return annotation
Remember: name: type / param: type / -> type documents the expected type. Python stores it and never checks it while running.
See also: optional and union types · mypy vs pyright

