Annotations That Express Intent
Annotations That Express Intent
Annotations document the types functions expect and return; Python stores them in __annotations__ but does not enforce them at runtime.
def retry(fn: Callable[[], T], attempts: int = 3) -> T: ...
counts: dict[str, int] = {} # PEP 585 builtins, Python 3.9+
opt: str | None = None # PEP 604 unions, Python 3.10+
Prefer builtin generics (list[int], tuple[str, int]) over legacy typing.List in modern code, use Optional[X]/X | None for possibly-absent values, and -> None for procedures. Annotations are the contract static checkers read — they do nothing when the program runs, so treat them as documentation plus verification, not protection.
Checking with mypy
Checking with mypy
Add a configuration block and run the checker in CI alongside tests:
# pyproject.toml
[tool.mypy]
python_version = "3.12"
strict = true # enables disallow_untyped_defs, warn_return_any, ...
files = ["src"]
mypy src # type-check the package
mypy --follow-imports=skip module.py # check one file with untyped deps
mypy is gradual: untyped functions are checked leniently unless --strict or per-module overrides demand annotations. Adopt incrementally — start with check_untyped_defs, then tighten modules you actively change. cast(T, value) and # type: ignore[assignment] are escape hatches; each one should carry a comment justifying it.
Pitfalls and Interview Angles
Pitfalls and Interview Angles
Annotations are erased from runtime behavior. Passing a wrong int where str is declared raises no error until some downstream string operation fails. Validate untrusted input with pydantic, dataclasses, or hand-written checks; let mypy guard internal logic.
Any spreads silently. One Any in a signature disables checking for every caller that value touches. Prefer object (must narrow before use), TypeVar, or a Protocol.
Interview angle: explain gradual typing — untyped legacy code coexists with strict modules — then contrast static checking with runtime validation and name a bug each one would catch that the other misses.
Quiz
1. What happens at runtime to a function annotated as def f(x: int) -> str when called with f("a")?
2. Which typing approach is current for a dict of string keys and list values?
3. Why is returning Any dangerous in strict mode?
Flashcards
Question
Gradual typing
Click to reveal answer
Answer
A system where typed and untyped code interoperate; checking strength is configurable per module.
Question
Optional[X] vs X | None
Click to reveal answer
Answer
Same meaning: value is X or None. The | syntax needs Python 3.10+ (or future import).
Question
mypy --strict
Click to reveal answer
Answer
Enables full checking: annotated defs, no Any leaks, warn_return_any, and more.
Question
Runtime vs static validation
Click to reveal answer
Answer
Annotations are static only; parse untrusted data with validators before trusting it.
Revision Notes
Key Takeaways
- 1. Annotations document contracts for tools; the interpreter never enforces them
- 2. Built-in generics and unions replaced typing.List/Optional for Python 3.9+/3.10+ code
- 3. Keep Any rare and validate external data at runtime with a real validator
Interview Tips
- • Say explicitly that type hints are erased from enforcement and name the tool that reads them instead
- • Show a gradual-typing adoption plan: strict config, new code strict, legacy modules tightened over time
Cheat Sheet
def f(x: int) -> str; list[int]/dict[str,int] (3.9+), X | None (3.10+); annotations not enforced; mypy --strict in CI; Any disables checks.
Continue Learning
Before You Start
- No prerequisites listed.