Initial extracted documentation set
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
# Module Design
|
||||
|
||||
Design a small, stable public surface. Keep implementation modules movable behind it.
|
||||
|
||||
## Why
|
||||
|
||||
Python makes it easy to publish internals by accident:
|
||||
|
||||
- every file is importable
|
||||
- helpers become de facto API once users depend on them
|
||||
- refactors turn into breaking changes when file layout becomes the contract
|
||||
|
||||
Mature libraries push back on that. They usually:
|
||||
|
||||
- publish one obvious import surface
|
||||
- re-export supported names deliberately
|
||||
- keep internal modules non-authoritative
|
||||
- use `__all__` when the boundary needs to be explicit
|
||||
|
||||
That buys two things:
|
||||
|
||||
- simpler imports for callers
|
||||
- freedom to reorganize internals later
|
||||
|
||||
## The pattern
|
||||
|
||||
1. Decide what callers should import.
|
||||
2. Re-export those names from the package boundary.
|
||||
3. Keep implementation details in internal modules.
|
||||
4. Use `__all__` when you want an explicit contract.
|
||||
5. Treat internal file layout as private unless you intentionally publish it.
|
||||
|
||||
## When to use
|
||||
|
||||
Use this when:
|
||||
|
||||
- a package spans multiple modules
|
||||
- internals will evolve faster than the public API
|
||||
- you want callers thinking in domain concepts, not filenames
|
||||
- compatibility matters across releases
|
||||
|
||||
## When not to use
|
||||
|
||||
Do not build a facade when:
|
||||
|
||||
- the package is tiny and direct imports are already clear
|
||||
- the abstraction boundary is still moving fast
|
||||
- re-exporting would turn `__init__.py` into a junk drawer
|
||||
|
||||
A curated surface is not the same as a flat surface. Keep structure where the concepts are meaningfully different.
|
||||
|
||||
## Preferred shapes
|
||||
|
||||
### Package facade over internal modules
|
||||
|
||||
```python
|
||||
# mypkg/__init__.py
|
||||
from ._client import Client
|
||||
from ._errors import AppError
|
||||
from ._models import Item
|
||||
|
||||
__all__ = ["Client", "AppError", "Item"]
|
||||
```
|
||||
|
||||
Why this works:
|
||||
|
||||
- callers learn one stable import path
|
||||
- internals can move without import churn
|
||||
- the package advertises its real contract
|
||||
|
||||
### Explicit module contract
|
||||
|
||||
```python
|
||||
__all__ = ["Number", "Complex", "Real", "Rational", "Integral"]
|
||||
```
|
||||
|
||||
This says: these names are supported; everything else is implementation detail.
|
||||
|
||||
## Counterexamples
|
||||
|
||||
### File layout becomes the API by accident
|
||||
|
||||
```python
|
||||
from mypkg.utils import helper_a
|
||||
from mypkg.impl_v2 import thing
|
||||
from mypkg.more_helpers import other_thing
|
||||
```
|
||||
|
||||
Refactoring internal modules now breaks users.
|
||||
|
||||
### Everything dumped into `__init__.py`
|
||||
|
||||
If `__init__.py` exports fifty unrelated names, you did not create a clean facade. You created autocomplete noise.
|
||||
|
||||
### Public API mirrors the folder tree too literally
|
||||
|
||||
If callers need to know today’s internal layout to use the library, the boundary is still underdesigned.
|
||||
|
||||
## Source signals
|
||||
|
||||
### CPython
|
||||
|
||||
- `Lib/numbers.py:8-23` warns that published ABC APIs are hard to change and should be designed carefully.
|
||||
- `Lib/numbers.py:35` publishes a narrow `__all__` rather than treating every helper as public.
|
||||
- `Lib/operator.py:13-15` and `Lib/smtplib.py:55-58` do the same in stdlib modules with mixed public/internal names.
|
||||
|
||||
### Pytest
|
||||
|
||||
- `src/pytest/__init__.py:6-80` builds the top-level `pytest` API by importing from many `_pytest.*` internals.
|
||||
- `src/pytest/__init__.py:98-186` then pins that facade with an explicit `__all__`.
|
||||
|
||||
### HTTPX
|
||||
|
||||
- `httpx/__init__.py:1-12` re-exports the package surface from internal modules such as `._client`, `._exceptions`, and `._models`.
|
||||
- `httpx/__init__.py:29-100` defines the supported top-level export list explicitly.
|
||||
- `httpx/__init__.py:103-106` rewrites exported objects’ `__module__` to `httpx`, reinforcing the facade instead of leaking internal filenames.
|
||||
|
||||
### Click
|
||||
|
||||
- `src/click/__init__.py:10-75` exposes the package through re-exports.
|
||||
- `src/click/__init__.py:77-126` keeps compatibility shims and deprecations at the boundary instead of freezing old internal layout forever.
|
||||
|
||||
## Bottom line
|
||||
|
||||
Make the public API intentional.
|
||||
|
||||
Callers should depend on your concepts, not your current file tree.
|
||||
Reference in New Issue
Block a user