120 lines
3.7 KiB
Markdown
120 lines
3.7 KiB
Markdown
# 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`.
|