Files
python-patterns/patterns/module-design.md
T
2026-06-01 21:42:05 +00:00

128 lines
3.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 todays 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.