Initial extracted documentation set

This commit is contained in:
Rodin
2026-06-01 21:42:05 +00:00
commit 60ffec18e4
17 changed files with 1590 additions and 0 deletions
+119
View File
@@ -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`.