Initial extracted documentation set
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
# 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`.
|
||||
Reference in New Issue
Block a user