Type Hints & Protocols
Type annotations (PEP 484) let you declare expected types for variables, arguments, and return values. They are not enforced at runtime by default.
Type hints = sticky notes on your fridge saying 'milk goes here.' Python won't stop you from putting juice there — it's just a suggestion. mypy is the roommate who actually reads the notes and yells at you.
def f(x: int) -> str: ... Type hints are metadata — Python doesn't check them at runtime. mypy, pyright, and pytype are static type checkers. from typing import List, Dict, Optional, Union, Callable, TypeVar, Generic. Optional[X] is Union[X, None]. Python 3.10+ uses X | None instead. TypedDict for typed dicts. Literal for specific values.
typing.Protocol (PEP 544, Python 3.8+) enables structural subtyping: a class satisfies a Protocol if it has the required methods, regardless of inheritance (duck typing + static checking). This is more Pythonic than ABC for defining interfaces. TypeVar and Generic enable generic classes. from __future__ import annotations defers annotation evaluation (PEP 563) so forward references work. Python 3.12 adds type keyword for type aliases. Runtime annotation access: typing.get_type_hints(func) evaluates string annotations.
Type hints are documentation that tools can verify. I use them for public APIs and complex logic — they make intent explicit and catch bugs with mypy/pyright before runtime. Optional[X] means 'X or None'. For structural typing (duck typing + static checks), use Protocol instead of ABC — a class satisfies a Protocol by having the right methods, no inheritance needed. This aligns with Python's duck-typing philosophy while adding static safety.
Type hints are NOT enforced at runtime. def f(x: int): pass; f('hello') runs fine. Use a runtime validation library (pydantic, beartype) if you need runtime enforcement. Also, using isinstance() checks for type hints at runtime is almost always wrong — that's what the static checker is for.