Building a public layout API from a library’s internal layout usually means one of two things today:
- Writing every exported name twice, once in an import and again as a string in
__all__. - The reflexive-alias idiom,
from x import y as y, which type checkers understand but which reads like a typo to everyone else, and which leaves__all__empty, so you losedir()cleanliness and wildcard-import control.
I’d like to propose a small addition that removes both: a from <module> export <name> [as <alias>] statement. It does exactly what from <module> import <name> [as <alias>] does, and also appends the name to __all__ in the same statement.
# spam/__init__.py
from ._internal.core export PublicAPI
from ._internal.widgets export Widget as PublicWidget
Nothing is left to sync by hand, and no alias needs decoding. It composes with control flow the same way import does, and supports a wildcard form for the common two-tier layout (an internal module curates its own __all__, a hub module does from ._internal.core export *), plus a lazy variant that builds on PEP 810.
Relationship to PEP 842: this grew directly out of that thread. Both proposals land on the same syntax for the module re-export case, from MODULE export NAME, arrived at independently, which I take as a good sign it’s the right shape. Where they differ: PEP 842 also adds export forms for standalone names, assignments, def, and class, backed by a new __export__ variable and an ExportWarning on non-exported attribute access. This proposal deliberately covers only the re-export case.
Relationship to PEP 844: just posted, and worth reading alongside this one. It proposes public()/private() builtins for names a module defines, the same ground atpublic covers today. Its own text notes that it doesn’t solve re-exports well (public(Widget=Widget) still names Widget three times) and calls this proposal a good companion for exactly that gap: public()/private() for names a module defines, export for names a module passes through. Its Open Issues section asks how PEP 842, 843, and 844 should be reconciled, so that’s probably the central question for this thread.
Quick comparison:
| PEP 842 | PEP 843 | PEP 844 | |
|---|---|---|---|
| Mechanism | New syntax (five forms of the export keyword) |
New syntax (one form of the export keyword) |
New builtins; no grammar change |
| Scope | Definitions and re-exports | Re-exports | Definitions |
| Export list | A new __export__ list, and __all__ |
__all__ |
__all__ |
| Runtime enforcement | Yes: ExportWarning on access to non-exported attributes |
None | None |
Full draft of the proposal with explanation is here: PEP 843 – Export Statement for DRY Re-exports | peps.python.org
Feedback welcome, especially on the one open question in the draft: should export * require the source module to define its own __all__, or fall back to import *'s no-all behavior (bind everything without a leading underscore)?
