# Typing Use types to describe accepted shapes and behavioral contracts, not to pretend Python is a different language. ## Why Good Python typing improves APIs in two ways: - callers can see which shapes are accepted - maintainers can preserve real boundaries without smearing `Any` everywhere The mature pattern is not “make everything maximally abstract.” It is: - use structural typing when capability matters more than inheritance - use explicit aliases and unions for ergonomic public inputs - keep public APIs typed even when internals stay dynamic That gives users a real contract without freezing implementation choices too early. ## The pattern 1. Type public APIs precisely. 2. Prefer `Protocol` when callers care about behavior, not ancestry. 3. Use explicit unions and aliases for user-facing flexibility. 4. Keep dynamic internals from leaking into the public contract. 5. Avoid `Any` unless you truly mean “anything goes.” ## When to use Use this when: - multiple implementations can satisfy one behavioral need - callers naturally have more than one valid input shape - you want strong editor and type-checker help at public boundaries - internals are dynamic but the public contract is still stable ## When not to use Do not use a protocol when a concrete type is the real contract. Do not use broad unions just to avoid choosing a better API. Do not over-trust `@runtime_checkable`: CPython is explicit that runtime protocol checks verify only attribute presence, not signature correctness. ## Preferred shapes ### Structural typing for capability-based contracts ```python class Writer(Protocol): def write(self, data: bytes) -> int: ... ``` If the caller only needs `write()`, do not require a specific base class. ### Explicit flexible public inputs ```python URLInput = URL | str ``` This is better than either extreme: - forcing callers to pre-wrap everything - accepting `Any` and hoping for the best ## Counterexamples ### Inheritance-only abstraction ```python class BaseStore: ... def persist(store: BaseStore) -> None: ... ``` This is too rigid when the function only needs a small capability surface. ### Type surrender ```python def send(data: Any, options: Any) -> Any: ... ``` The API contract disappeared. ### Runtime protocol overconfidence If runtime safety matters, attribute-presence checks are not enough. Protocols do most of their work at static-analysis time. ## Source signals ### CPython / typing - `Lib/typing.py:2132-2157` defines `Protocol` around structural subtyping and explicitly frames it as static duck typing. - `Lib/typing.py:2155-2157` states that `@runtime_checkable` protocols check only attribute presence, ignoring type signatures. - `Lib/typing.py:2190-2250` shows `Annotated` as “type plus metadata,” not a new underlying runtime type. ### HTTPX - `httpx/_client.py:639-660` gives `Client` precise constructor types for auth, params, headers, cookies, timeouts, transports, and `base_url`. - `httpx/_client.py:1353-1374` mirrors that precision on `AsyncClient` instead of collapsing to untyped arguments. ### Pydantic - `pydantic/main.py:156-205` exposes typed `ClassVar[...]` metadata for config, fields, serializer, and validator state even though framework internals are dynamic. - `pydantic/main.py:253-264` makes model construction validate `**data: Any` immediately instead of pretending arbitrary inputs are already safe. - `pydantic/main.py:721-768` gives `model_validate(...) -> Self` an explicit typed boundary contract. ## Bottom line Use typing to make public boundaries clearer. Be flexible where callers need flexibility. Be precise where contracts matter. Do not hide uncertainty behind `Any`.