From e7fdebefab6b41952c0ca3f69a9c4804a80cffa4 Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Wed, 5 Aug 2026 19:17:12 -0700 Subject: [PATCH 01/12] PEP 844: First draft --- peps/pep-0844.rst | 666 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 666 insertions(+) create mode 100644 peps/pep-0844.rst diff --git a/peps/pep-0844.rst b/peps/pep-0844.rst new file mode 100644 index 00000000000..fae715a6336 --- /dev/null +++ b/peps/pep-0844.rst @@ -0,0 +1,666 @@ +PEP: 844 +Title: ``public`` and ``private`` builtins +Author: Barry Warsaw +Discussions-To: Pending +Status: Draft +Type: Standards Track +Created: 05-Aug-2026 +Python-Version: 3.16 +Post-History: Pending + + +Abstract +======== + +This PEP proposes adding two new builtin functions, ``public()`` and ``private()``, which document +the public interface of a module by keeping its ``__all__`` synchronized with the names actually +defined in that module. Both functions double as decorators (e.g. ``@public`` and ``@private``) +which can decorate class and function definitions, so that a name's visibility is declared exactly +once, at the point where the name is defined. + +For example: + +.. code-block:: python + + # spam.py + @public + class Public: + ... + + @private + class Private: + ... + + public(SEVEN=7) + +.. code-block:: pycon + + >>> import spam + >>> spam.__all__ + ['Public', 'SEVEN'] + +The proposed semantics are exactly those of the third party `atpublic +`__ package, which has provided this functionality since 2016. + +This PEP is an adjunct to :pep:`842` and PEP 843; see `Relationship to PEP 842 and PEP 843`_. + + +Motivation +========== + +The `module global variable `_ +``__all__`` is the mechanism Python currently defines for declaring a module's public name. +However, ``__all__`` suffers from a well known problem: it is typically defined as a separate list +often far from the objects whose names are contained in it. An object defined at one point in the +file is repeated as a string literal in an ``__all__`` list somewhere else, usually at the top of +the file. + +Nothing keeps the two in sync, leading to these problems: + +* Names get added to the module but never added to ``__all__``. +* Names get removed from or renamed in the module but not in ``__all__``, so ``from spam import *`` + raises :exc:`AttributeError`. +* It's easy to typo a name (or leave out a list item delimiting comma) in ``__all__``. +* Drift is only detectable in one direction. A linter can flag a name in ``__all__`` that doesn't + exist as an object in the module, but no tool can flag a public name missing from ``__all__``, + because nothing in the source says that name was meant to be public. +* Readers of the code must scroll to a different part of the file (or a different screen) to answer + "is this name public?" + +The convention of prefixing private names with an underscore addresses a related but different +problem, and :pep:`842` describes at length why `prefixing is not by itself a sufficient answer +`_. + +The pattern proposed here -- declaring visibility at the definition site with a decorator -- is not +new or speculative. The ``atpublic`` package on PyPI has implemented it for a decade, and it is +already depended on by a number of projects. What this PEP proposes is that the pattern is common +enough, and useful enough, to be spelled without a third party dependency. Thus it proposes to add +``atpublic``'s ``public()`` and ``private()`` functions to the builtins. + + +.. _pep-844-all-is-normative: + +``__all__`` already defines the public API +------------------------------------------ + +It is sometimes said that Python has no way to express which names in a module are public and which +are private, and that ``__all__`` is merely a convention governing ``from spam import *``. However, +the :ref:`language reference ` explicitly says: + + The *public names* defined by a module are determined by checking + the module's namespace for a variable named ``__all__``; if defined, + it must be a sequence of strings which are names defined or imported + by that module. [...] The names given in ``__all__`` are all + considered public and are required to exist. If ``__all__`` is not + defined, the set of public names includes all names found in the + module's namespace which do not begin with an underscore character + (``'_'``). ``__all__`` should contain the entire public API. It is + intended to avoid accidentally exporting items that are not part of + the API (such as library modules which were imported and used within + the module). + +This PEP explicitly adopts the definition of "public" in the Python Language Reference. Following +from this: + +**The concept already exists and is normative.** "Public name" is a term the language reference +defines, and it defines it in terms of ``__all__``. This is specification text, not folklore. +Python is not missing a way to say what is public; it has one, and it is documented. + +**Exhaustiveness is already the contract.** "``__all__`` should contain the entire public API" is +unambiguous. A module whose ``__all__`` lists only part of its public surface is not exercising +some alternative reading -- it is out of conformance with what the reference says ``__all__`` means. + +**The imported-module problem is already in scope.** The reference names it outright: ``__all__`` +exists in part to avoid "accidentally exporting items that are not part of the API (such as library +modules which were imported and used within the module)." A module that declares ``__all__`` +accurately and imports say ``argparse``, does not leak the name ``argparse`` as part of *its* public +API, with or without renaming the import to ``_argparse``. + +The gap, then, is not semantic but ergonomic. Python already specifies what it means for a name to +be public, and already recommends that ``__all__`` say so exhaustively, while providing no +convenient way to keep that promise as a module evolves. It asks authors to maintain a list of +string literals by hand, in a different part of the file from the definitions, which is subject to +being quite error prone. + +This PEP supplies the missing ergonomics. It does not redefine what it means to be "public", or +introduce a second notion of visibility, or change what ``__all__`` already means. It doesn't try +to redefine what it means for a name to be exported. It does however make the documented contract +easy enough to actually honor. + + +Specification +============= + +Two new builtins are added: ``public()`` and ``private()``. + +``public()`` +------------ + +``public()`` has two call forms. The decorator form (``@public``) is the most common use. + +**Decorator form.** When called with a single positional argument that has both a ``__module__`` and +a ``__name__`` attribute -- i.e. a function or a class -- ``public()`` appends that object's +``__name__`` to the ``__all__`` of the module named by its ``__module__``, and returns the object +unchanged: + +.. code-block:: python + + @public + def foo(): + ... + + @public + class Bar: + ... + + # __all__ == ['foo', 'Bar'] + +Note that the bare decorator is used; Python's semantics are to implicitly pass the object it +decorates as the first argument to the decorator function. + +**Function call form.** Names which cannot be decorated, such as constants, instances, and aliases, +are declared by calling ``public()`` with keyword arguments. Each keyword binds its value in the +calling module's globals *and* appends the name to ``__all__``: + +.. code-block:: python + + public(SEVEN=7) + public(a_bar=Bar()) + public(ONE=1, TWO=2) + +The value of a single keyword argument is returned; for multiple keyword arguments, a tuple of the +values is returned in order: + +.. code-block:: python + + a, b, c = public(a=3, b=2, c=1) + d = public(d=9) + +In all cases, if the module does not already define ``__all__``, ``public()`` creates it as an empty +:class:`list` before appending. If ``__all__`` exists but is not a list, :exc:`ValueError` is +raised. Any strings already present in an existing ``__all__`` are left in the list. + + +``private()`` +------------- + +``private()`` (used exclusively as ``@private``) is the dual of the decorator form of ``public()``. +It documents that a name is *not* part of the module's public interface, and guarantees that the +name does not appear in ``__all__``, removing it if it is already present. The decorated object is +returned unchanged: + +.. code-block:: python + + @private + def helper(): + ... + +The treatment of ``__all__`` is the same as with ``public()``, with regards to creation if missing +and raising a :exc:`ValueError` if present but not a list. However, ``@private`` *does* mutate an +existing ``__all__`` by removing the name of the decorated object. + +.. note:: + + ``private()`` does *not* support a function call API as no valid use case for that form has been + identified or requested by users of the ``atpublic`` module. See the :ref:`Open Issues` for + further discussion. + + +Restrictions +------------ + +Note that because ``@public`` is primarily used to keep the ``__all__`` module global in sync, only +module level objects may be decorated. Decorating a method inside a class body is not supported, +since ``__all__`` documents module contents, not class contents. + +Because ``__all__`` must be mutable for these functions to append to it, a module that assigns +``__all__`` itself must assign a list. A module that wants an immutable ``__all__`` can freeze it +after the last declaration with ``__all__ = tuple(__all__)``. + + +Rationale +========= + +Why builtins? +------------- + +The declaration of a module's public interface is a fundamental and often requested property, +especially as code bases grow. Many use cases have been identified in discussions, and different +approaches have been developed in different libraries and applications. Enough experience has been +gained over the decade of ``atpublic``'s existence that requiring a third party dependency (or an +import at the top of every module) to spell something this fundamental is friction that discourages +its use. It is also awkward in exactly the places where it matters most: the standard library +itself, and small single file modules. + +``atpublic`` acknowledges this today by offering an optional install step that injects ``public`` +and ``private`` into :mod:`builtins` via a ``.start``/``.pth`` file, so that no import is needed. +That this exists at all is evidence that builtins is the most convenient location for these +utilities. + + +Why decorators? +--------------- + +Declaring visibility at the definition site is the entire point. The name appears exactly once, so +it cannot drift out of sync; refactoring tools rename it correctly for free; and a reader can answer +"is this public?" without leaving the line they are on. Using a decorator significantly reduces the +need to repeat yourself. + + +.. _pep-844-static-analysis: + +Static analysis of the keyword form +----------------------------------- + +The strongest objection to this proposal concerns the keyword call form, and it is worth stating +explicitly. Given: + +.. code-block:: python + + public(SEVEN=7) + +``SEVEN`` is bound in the module's globals by a function that reaches into its caller's frame. +Nothing about that binding is visible in the syntax tree. A type checker, linter, or language +server reading the source sees a bare function call and no assignment, and will therefore report +``SEVEN`` as undefined at every use site. A soft keyword like ``export SEVEN = 7`` (such as +proposed by :pep:`842`) has no such problem, because syntax is by construction visible to anything +that parses the file. This, and not the DRY objection raised in :pep:`842`, is the real cost of +choosing a builtin over a keyword. + +This could easily be alleviated by future modifications to linting tools, so that they explicitly +recognize the function call form of ``public()``. This would be a one-time, bounded cost paid by a +handful of tools, not an ongoing cost paid by every Python programmer. + +``public()`` is not an arbitrary function performing mysterious magic. It is a builtin with a +small, fixed, specified signature, and its effect on the module namespace is fully determined by the +keyword names at the call site, which are *literally present in the source*. Teaching a checker +that ``public(SEVEN=7)`` binds ``SEVEN`` and appends ``"SEVEN"`` to ``__all__`` is a simple analysis +than these tools can easily perform. + +There is direct precedent. Static analyzers already model ``__all__`` mutation beyond simple +assignment, including ``__all__ += [...]`` and ``__all__.append(...)``, precisely because real code +does this. They already special-case namespace-creating callables whose behavior is not evident +from the grammar, such as :func:`~collections.namedtuple`, :class:`~typing.TypedDict`, and +:func:`~dataclasses.dataclass`. Adding ``public()`` to that list is an increment on work these +tools have already done, not a new category of problem. + +If this PEP is accepted, that support is expected to follow quickly, for the ordinary reason that +tools support what the language provides. In the interim (and for older tool versions) the return +value of ``public()`` gives an entirely explicit spelling that requires no special support at all: + +.. code-block:: python + + SEVEN = public(SEVEN=7) + +Here the binding is a plain assignment, visible to every tool that parses Python. This form is a +transition aid rather than the recommended spelling, and it should not be needed for long. + +The conclusion is that for the data and type alias use cases, which are the places a decorator +genuinely cannot be utilized, do not require new syntax at all. They require a function call that +tools can learn to read, alleviating the need for a dedicated, new ``export`` keyword. + + +.. _pep-844-urgency: + +Is this urgent? +--------------- + +`Guido van Rossum `__ raised this question about :pep:`842`, +and it applies with equal force here: + + But Python has existed without this feature for over 35 years -- is + it really urgent? Remember the Zen of Python, which says "Now is + better than never. Although never is often better than *right* + now." + +No. This PEP is not urgent, and it does not claim to be. Nothing about module name visibility, or +the exported, public API of a module is. But urgency is the wrong test to apply to this particular +proposal, for three reasons. + +**The feature is not new.** This PEP does not ask Python to adopt an untried idea; ``atpublic`` has +implemented these exact semantics since 2016. The question is not "should Python have this?" since +users who want it already have it, but "should having it cost a third party dependency?" A decade +of production use is the opposite of rushing. It has already surfaced and settled the corner cases, +syntax, and semantics a fresh design would have to guess at: that only module-level objects can be +decorated, what to do about a non-list ``__all__``, and what the functional form should return. + +**The cost of being wrong is low.** The urgency argument has the most weight against changes that +cannot be walked back. Syntax is permanent: a soft keyword constrains the grammar forever, must be +taught to every future Python programmer, and is unavailable to any module supporting an older +interpreter. A new module level variable with runtime consequences changes the observable behavior +of code without warning. A builtin function is the cheapest thing in this design space on both +counts: it is inert until called, it changes nothing about modules that ignore it, and if it proves +to be a mistake it can be deprecated in the ordinary way without touching the grammar. + +**The sequencing matters more than the timing.** Three proposals in this cycle address the same +problem space, and two of them ask for new syntax. If Python is going to change its grammar to +address this need, that decision should be made *after* weighing the option that requires no grammar +change, not before. Once an ``export`` keyword exists, builtins covering the same ground are +redundant and will never be added, regardless of whether they were the better answer. That +asymmetry is the reason to consider this PEP now rather than later: not because the feature is +pressing, but because the cheaper alternative stops being available once the expensive one lands. + + +.. _pep-844-performance: + +Import time performance +----------------------- + +When this idea was informally floated with core developers some years ago, before either :pep:`842` +or PEP 843 existed, the objection raised was not the design but the cost weighed against its +utility: decorators runs at import time, once per decorated name, and CPython's startup time is a +closely watched number. The concern is legitimate and deserves a direct answer. + +**The work per call is small and bounded.** ``public()`` in decorator form reads the decorated +object's ``__name__``, obtains the defining module's globals, creates ``__all__`` as an empty list +if needed, and appends one string. There is no complicated introspection, no allocation or work +proportional to module size, and no I/O. Whatever the constant factor turns out to be, it does not +grow with the size of the module. + +**The cost is opt-in and proportional to the public API.** A module that does not call ``public()`` +pays nothing at all, unlike a change to module attribute access, which affects every module whether +or not it participates. A module that does call it pays once per *public* name, and a module's +public surface is typically a small fraction of the names it defines. + +**Syntax is not free either.** It is worth being precise about what the alternative saves. +:pep:`842`'s ``export`` statement is specified to check that the name exists in globals, create +``__export__`` if absent, and call ``list.append`` -- the same operations, expressed in bytecode +rather than a call. The saving is the function call dispatch, not the underlying work. That is a +real difference, but it is a constant factor on an already small constant, not a difference in kind. + +**A C implementation is feasible and fast.** This is the point on which a builtin is strictly better +positioned than the third-party package. ``atpublic`` shipped a C implementation of ``public()`` +for a time, and it was substantially faster than the pure Python version. It was ultimately +dropped, not because it did not work, but because requiring a compiled extension module in a third +party package is a significant packaging and installation burden for a library this small -- a +burden borne entirely so that the pure Python fallback could be avoided. + +That trade-off does not exist in CPython. A builtin is compiled as part of the interpreter, so the +fast implementation is simply *the* implementation, with no wheel platform support matrix, no +fallback path, and no optional extra. Moreover, a C implementation inside the interpreter can do +less work than any third party one: the decorator form can access the calling frame's globals +directly rather than going through ``__module__`` and a :data:`sys.modules` lookup, and the function +call form needs no Python-level stack inspection. + +The argument is therefore somewhat the reverse of the original objection. The performance concern +is a reason to put ``public()`` in builtins where it can be made fast, rather than a reason to leave +it on PyPI, where it cannot. + +.. note:: + + This section argues that the cost is acceptable; it does not yet demonstrate it. Measurements + against CPython's startup benchmarks, for both a decorated standard library and a synthetic worst + case, should accompany the reference implementation. See `Open Issues`_. + + +Relationship to PEP 842 and PEP 843 +=================================== + +TL;DR: :pep:`842` proposes adding an ``export`` keyword and a new module global ``__export__`` +variable. PEP 843 proposes adding a ``from ... export ...`` form. + + +Two problems, not one +--------------------- + +Discussion of module visibility addresses two separable problems: + +1. **Bookkeeping.** A name's visibility as public or private is declared in a different place from + where the object so named is defined, so the declaration drifts out of sync with the + implementation. This is a problem about *where you add the declaration*. + +2. **Runtime consequences.** ``__all__`` governs only ``from spam import *``. It has no effect on + attribute access, :func:`dir`, :func:`help`, or autocompletion, so a name that is intended to be + kept private is indistinguishable from public names to these patterns of module introspection. + This is a problem about *what the declaration does*. + +This PEP addresses only the first. It takes the position that the first problem is the more urgent +and the more broadly applicable of the two, that it can be solved without new syntax and without a +new variable, and that solving it does not commit Python to any particular answer to the second. + + +.. _pep-844-why-all: + +Why ``__all__`` and not ``__export__`` +-------------------------------------- + +:pep:`842` proposes a new ``__export__`` variable. This PEP proposes to keep using ``__all__``. + +:pep:`842` gives two reasons why ``__all__`` is inadequate. The first is that ``__all__`` drifts +out of sync with the module. That is true, and it is precisely the problem ``atpublic`` and this +PEP solves. However a *new list of string literals in the same place in the file* does not directly +solve this problem. :pep:`842`'s own revision history concedes the point, quoting `Guido van Rossum +`__ on the original ``__export__``-only design: + + But the ergonomics are similar to those of ``__all__``, and those + are bad. It's too easy to forget to add (or remove!) something to + the list, and it's distracting to have to update the export info in + a totally different part of a file than the definition of the + exported thing. + + If we just cared about classes and functions, a more ergonomic + approach would be an ``@export`` decorator. If we also care about + exporting data or type aliases, I'd much rather look for a solution + that adds a soft keyword named ``export`` (or ``private``, for a + better default). + +Drift is a property of *declaring at a distance*, not a property of ``__all__``. Any variable +maintained by hand has it, and no variable maintained at the definition site does. + +The first half of that quote is the argument this PEP is built on, and the second half names the +decorator as the ergonomic answer for classes and functions. The remaining question -- what to do +about data and type aliases, where there is nothing to decorate -- is addressed by PEP 844 in +:ref:`pep-844-static-analysis`. + +The second reason is that ``__all__`` is not always exhaustive in practice. A module may +deliberately keep a public type alias out of ``__all__`` to avoid polluting wildcard-importing +namespaces, so its public API can end up a *superset* of what ``__all__`` lists. + +That is an accurate observation about existing code, but it is a weaker argument than it first +appears, because it describes a *deviation from the specification* rather than an alternative +reading of it. As :ref:`pep-844-all-is-normative` sets out, the language reference already states +that ``__all__`` "should contain the entire public API." A module that withholds public names from +``__all__`` is not asserting that ``__all__`` means something narrower than the public API; it is +trading conformance away for control over ``import *``. + +What that trade exposes is a real flaw, but a different one from the one :pep:`842` diagnoses: +``__all__`` does double duty. It is at once the declaration of what is public and the control +surface for wildcard imports, and when those two purposes conflict, authors sacrifice the +declaration because only the wildcard behavior has any teeth. + +Introducing ``__export__`` does not repair that conflation. It leaves ``__all__`` doing both jobs, +adds a second declaration to keep synchronized with the first, and transfers the word "public" to +the new module variable, while the language reference continues to define it in terms of +``__all__``. A module conscientious enough to maintain ``__export__`` accurately would have been +conscientious enough to maintain ``__all__`` accurately; the ones that drift will drift in both. + +This PEP takes no position on whether unexported-name warnings are desirable. It observes only that +the bookkeeping question is separable from the runtime-semantics question, and it answers the +former. ``public()`` populates a list; if Python later decides that some list should carry runtime +consequences, ``public()`` can populate that one instead, or both. Nothing here closes the door on +:pep:`842`. + + +Why PEP 843 is a good companion +------------------------------- + +This PEP does **not** solve the DRY problem for re-exports, and cannot do so gracefully. A "hub +module" that pulls names out of private submodules must currently write each name twice: + +.. code-block:: python + + from ._core import Widget + public(Widget=Widget) + +Notice how often ``PublicThing`` is named here. That's a big violation of DRY! + +The decorator form of ``@public`` is unavailable here because there is nothing to decorate, and the +function call form of ``public()`` requires naming the binding explicitly. This is exactly the gap +PEP 843 identifies, and its ``from ._core export Widget`` spelling closes it in a way no decorator +can. + +The two proposals therefore partition the problem cleanly, and provide excellent synergy: + +* ``public()`` and ``private()`` handle the names a module **defines**. +* ``from export `` handles imported names a module *passes through**. + +Both populate ``__all__``. Neither requires the other, and neither requires new runtime semantics +for the result. + +.. note:: + + PEP 843 was published as this PEP was being drafted, and :pep:`842` has since grown an ``export`` + statement of its own that overlaps both this PEP and PEP 843. The relationship between all three + needs to be settled on the discussion thread; see `Open Issues`_. + + +Backwards Compatibility +======================= + +Adding names to :mod:`builtins` shadows nothing, but it does mean that modules which define their +own module level ``public`` or ``private`` names will shadow the builtins instead. This is the same +situation as any other builtin (``id``, ``type``, ``list``), and is well understood. + +Code that imports ``public`` and ``private`` from the ``atpublic`` package will continue to work +unchanged (as long as the semantics continue to match), since an explicit import shadows the +builtin. + +Modules using these builtins will not run on Python 3.15 and earlier without either a dependency on +``atpublic`` or a compatibility shim. + + +Security Implications +===================== + +This PEP has no known security implications. Like ``__all__`` itself, ``public()`` and +``private()`` are documentation, not access control. + + +How to Teach This +================= + +``public()`` and ``private()`` would be documented alongside the other builtins, and referenced from +the tutorial section on modules where ``__all__`` is introduced. + +The rule to teach is a single sentence: decorate a name with ``@public`` if users of your module are +meant to use it, and don't decorate it (or decorate it with ``@private``, to say so explicitly) if +they aren't. + + +Reference Implementation +======================== + +The `atpublic `__ package, available on PyPI and maintained since +2016, implements the proposed semantics in pure Python. Its source is at +https://gitlab.com/flufl/public. + +A CPython implementation has not yet been written. + +For a time, ``atpublic`` also included a C implementation of ``public()``, which was considerably +faster than the pure Python one. It was dropped for packaging reasons that do not apply to a +builtin. See :ref:`pep-844-performance`. + + +Rejected Ideas +============== + +New ``export`` syntax instead of decorators +------------------------------------------- + +:pep:`842` proposes an ``export`` soft keyword covering the same ground as this PEP -- ``export +def``, ``export class``, ``export NAME = value``. Its `Rejected Ideas +`_ section +considers builtin ``public`` and ``private`` decorators, describes them as the author's next +preferred alternative to syntax, and rejects them on the grounds that "there's no easy way to export +simple variables without duplicating the name." + +That objection doesn't fully apply to the design proposed here. The keyword call form exists +precisely for the undecoratable cases, and writes the name exactly once: + +.. code-block:: python + + public(SEVEN=7) + +is the whole declaration. The name ``SEVEN`` is bound in the module globals, assigned the integer 7 +value, and appended to ``__all__``. There is no separate assignment to keep in sync. Compare +``export SEVEN = 7``: the two spellings carry the same information, cost roughly the same +keystrokes, and differ only in that one of them requires a grammar change. + +The substantive version of the objection is not about keystrokes but about tooling: a soft keyword +is visible to static analysis, while a function call that binds through its caller's frame is not. +That is a real cost, and it is answered in :ref:`pep-844-static-analysis`. + +The general argument holds beyond this example. New syntax is the most expensive thing Python can +add: it must be taught, it cannot be back-ported, it constrains the grammar permanently, and it is +unavailable to every module that must still run on an older interpreter. A builtin costs none of +that, is trivially shimmed on old versions, and (as is the case here) has a decade of usage +experience behind it. + + +Add a new ``__export__`` variable +--------------------------------- + +See :ref:`pep-844-why-all`. + + +Leave it on PyPI +---------------- + +Leaving ``atpublic`` on PyPI is certain a status quo decision that leaves the solution in the same +state it's currently in. Users who want to opt into this functionality can simply add that library +as a dependency and import the functions (or use the ``pip install atpublic[install]`` extra to +populate builtins). + +However, if this *is* a problem worth solving now, then leaving this in a third-party module on PyPI +doesn't serve our users adequately. The need to include a dependency and an explicit important may +be just enough of a hurdle (albeit small) to stop widespread use of it. Adding it to builtins gives +this pattern a promotional endorsement that will gain in popularity. + + +A new standard library module instead of builtins +------------------------------------------------- + +This would eliminate the third-party dependency problem, but still leaves the explicit import +usability cost. In addition, there's no obvious place to add it to the stdlib *other than* in +builtins. Two functions likely isn't worth the cost of a new top-level module. Besides, since +``__all__`` is in a sense built into Python, these functions should be built in too. + + +Open Issues +=========== + +* How should this PEP, :pep:`842`, and PEP 843 be reconciled? All three now contain a + definition-site or re-export declaration mechanism, and the overlap needs to be resolved before + any of them can sensibly be accepted. +* Should ``populate_all()``, ``atpublic``'s heuristic "infer ``__all__`` from what's defined here" + function, also be included? This is deferred for now; a heuristic is a harder case to make for a + builtin than the two explicit declarations are, and is less essential for improving module + visibility ergonomics. +* Should ``private()`` support a keyword call form, for symmetry? ``atpublic`` does not provide one + and no need for it has ever been demonstrated or requested. +* Should the standard library itself adopt these decorators, and if so on what schedule? This + question is entangled with :ref:`pep-844-performance` and should be settled with startup + measurements in hand. Also, as with all new capabilities (such as lazy imports), Python's policy + is generally not to wholesale update the stdlib to embrace the new functionality. These new + functions can be utilized opportunistically in modules where the most benefit can be gained, or + when a module undergoes substantial rewrite. +* Import time benchmarks for the C implementation are outstanding. + + +Acknowledgements +================ + +Thanks to Peter Bierma and Neil Girdhar, whose :pep:`842` and PEP 843 prompted this proposal, and to +the contributors to and users of ``atpublic`` over the past decade. + + +Change History +============== + +TBD + +Copyright +========= + +This document is placed in the public domain or under the +CC0-1.0-Universal license, whichever is more permissive. From 9565c643c309835800e4dd8b0f56093fbcef66df Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Wed, 5 Aug 2026 20:12:02 -0700 Subject: [PATCH 02/12] Grammar and accuracy passes --- peps/pep-0844.rst | 172 ++++++++++++++++++++++++++++++---------------- 1 file changed, 114 insertions(+), 58 deletions(-) diff --git a/peps/pep-0844.rst b/peps/pep-0844.rst index fae715a6336..257fd353674 100644 --- a/peps/pep-0844.rst +++ b/peps/pep-0844.rst @@ -14,9 +14,10 @@ Abstract This PEP proposes adding two new builtin functions, ``public()`` and ``private()``, which document the public interface of a module by keeping its ``__all__`` synchronized with the names actually -defined in that module. Both functions double as decorators (e.g. ``@public`` and ``@private``) -which can decorate class and function definitions, so that a name's visibility is declared exactly -once, at the point where the name is defined. +defined in that module. Both are used as decorators (``@public`` and ``@private``) on class and +function definitions, so that a name's visibility is declared exactly once, at the point where the +name is defined. ``public()`` additionally has a function call form for names that cannot be +decorated, such as constants. For example: @@ -39,7 +40,7 @@ For example: >>> spam.__all__ ['Public', 'SEVEN'] -The proposed semantics are exactly those of the third party `atpublic +The proposed semantics are those of the third party `atpublic `__ package, which has provided this functionality since 2016. This PEP is an adjunct to :pep:`842` and PEP 843; see `Relationship to PEP 842 and PEP 843`_. @@ -49,7 +50,7 @@ Motivation ========== The `module global variable `_ -``__all__`` is the mechanism Python currently defines for declaring a module's public name. +``__all__`` is the mechanism Python currently defines for declaring a module's public names. However, ``__all__`` suffers from a well known problem: it is typically defined as a separate list often far from the objects whose names are contained in it. An object defined at one point in the file is repeated as a string literal in an ``__all__`` list somewhere else, usually at the top of @@ -68,8 +69,8 @@ Nothing keeps the two in sync, leading to these problems: "is this name public?" The convention of prefixing private names with an underscore addresses a related but different -problem, and :pep:`842` describes at length why `prefixing is not by itself a sufficient answer -`_. +problem, and :pep:`842` describes at length why :pep:`prefixing is not by itself a sufficient +answer <842#prefixed-names-aren-t-necessarily-a-great-solution>`. The pattern proposed here -- declaring visibility at the definition site with a decorator -- is not new or speculative. The ``atpublic`` package on PyPI has implemented it for a decade, and it is @@ -113,7 +114,7 @@ some alternative reading -- it is out of conformance with what the reference say **The imported-module problem is already in scope.** The reference names it outright: ``__all__`` exists in part to avoid "accidentally exporting items that are not part of the API (such as library modules which were imported and used within the module)." A module that declares ``__all__`` -accurately and imports say ``argparse``, does not leak the name ``argparse`` as part of *its* public +accurately and imports ``argparse`` does not leak the name ``argparse`` as part of *its* public API, with or without renaming the import to ``_argparse``. The gap, then, is not semantic but ergonomic. Python already specifies what it means for a name to @@ -140,8 +141,8 @@ Two new builtins are added: ``public()`` and ``private()``. **Decorator form.** When called with a single positional argument that has both a ``__module__`` and a ``__name__`` attribute -- i.e. a function or a class -- ``public()`` appends that object's -``__name__`` to the ``__all__`` of the module named by its ``__module__``, and returns the object -unchanged: +``__name__`` to the ``__all__`` of the module in which ``public()`` is called, and returns the +object unchanged: .. code-block:: python @@ -176,9 +177,13 @@ values is returned in order: a, b, c = public(a=3, b=2, c=1) d = public(d=9) -In all cases, if the module does not already define ``__all__``, ``public()`` creates it as an empty +In all cases, ``public()`` modifies only the ``__all__`` of the module in which it is called. No +other module's ``__all__`` is ever affected. + +If the module does not already define ``__all__``, ``public()`` creates it as an empty :class:`list` before appending. If ``__all__`` exists but is not a list, :exc:`ValueError` is -raised. Any strings already present in an existing ``__all__`` are left in the list. +raised. Any strings already present in an existing ``__all__`` are left in the list. Appending is +idempotent, so a name that already appears in ``__all__`` is not added a second time. ``private()`` @@ -195,14 +200,25 @@ returned unchanged: def helper(): ... -The treatment of ``__all__`` is the same as with ``public()``, with regards to creation if missing -and raising a :exc:`ValueError` if present but not a list. However, ``@private`` *does* mutate an -existing ``__all__`` by removing the name of the decorated object. +Unlike ``public()``, ``private()`` never creates ``__all__``. If the module does not define +``__all__``, ``@private`` has no effect on the module namespace at all; it serves purely to +document the author's intent at the point of definition. If ``__all__`` does exist it must be a +list, or :exc:`ValueError` is raised, and the decorated object's name is removed from it if present. + +``@private`` deliberately does not create an empty ``__all__``, because doing so would silently +change the meaning of ``from spam import *``. With no ``__all__``, a wildcard import binds every +name not beginning with an underscore; with ``__all__ = []`` it binds nothing. A decorator whose +purpose is documentation should not have that effect. + +It follows that ``@private`` alone does not exclude a name from ``from spam import *``. Excluding +names is the job of ``@public``: as soon as any name in the module is marked public, ``__all__`` +exists, and everything not marked public is excluded automatically. ``@private`` records the +author's intent; ``@public`` is what makes that intent observable. .. note:: - ``private()`` does *not* support a function call API as no valid use case for that form has been - identified or requested by users of the ``atpublic`` module. See the :ref:`Open Issues` for + ``private()`` does *not* support a function call form, as no valid use case for it has been + identified or requested by users of the ``atpublic`` module. See `Open Issues`_ for further discussion. @@ -232,27 +248,38 @@ import at the top of every module) to spell something this fundamental is fricti its use. It is also awkward in exactly the places where it matters most: the standard library itself, and small single file modules. -``atpublic`` acknowledges this today by offering an optional install step that injects ``public`` -and ``private`` into :mod:`builtins` via a ``.start``/``.pth`` file, so that no import is needed. -That this exists at all is evidence that builtins is the most convenient location for these -utilities. +``atpublic`` acknowledges this today by offering an optional install step +(``pip install atpublic[install]``) that injects ``public`` and ``private`` into :mod:`builtins` at +interpreter startup, so that no import is needed. That this exists at all is evidence that +builtins is the most convenient location for these utilities. Why decorators? --------------- -Declaring visibility at the definition site is the entire point. The name appears exactly once, so -it cannot drift out of sync; refactoring tools rename it correctly for free; and a reader can answer -"is this public?" without leaving the line they are on. Using a decorator significantly reduces the -need to repeat yourself. +A decorator puts the declaration exactly where the definition is, which is the entire point. + +The mechanical benefit is that the name appears only once. It cannot drift out of sync, +refactoring tools rename it correctly for free, there is no second list to maintain, and the need +to repeat yourself largely disappears. + +The documentary benefit matters just as much. ``@public`` and ``@private`` record the author's +intent on the line a reader is already looking at. Answering "is this part of the API?" takes no +scrolling to a list elsewhere in the file, no cross-checking that list against the definitions, and +no guessing about whether a leading underscore was deliberate. The declaration stops being +bookkeeping attached to the definition and becomes part of it. + +``@private`` demonstrates this most clearly. In a module with no ``__all__`` it does nothing +mechanically at all: it adds no name, removes no name, and changes no behavior. Its entire value +is to say, at the point of definition, that the name is deliberately not public. .. _pep-844-static-analysis: -Static analysis of the keyword form ------------------------------------ +Static analysis of the function call form +----------------------------------------- -The strongest objection to this proposal concerns the keyword call form, and it is worth stating +The strongest objection to this proposal concerns the function call form, and it is worth stating explicitly. Given: .. code-block:: python @@ -275,7 +302,7 @@ handful of tools, not an ongoing cost paid by every Python programmer. small, fixed, specified signature, and its effect on the module namespace is fully determined by the keyword names at the call site, which are *literally present in the source*. Teaching a checker that ``public(SEVEN=7)`` binds ``SEVEN`` and appends ``"SEVEN"`` to ``__all__`` is a simple analysis -than these tools can easily perform. +that these tools can easily perform. There is direct precedent. Static analyzers already model ``__all__`` mutation beyond simple assignment, including ``__all__ += [...]`` and ``__all__.append(...)``, precisely because real code @@ -295,7 +322,7 @@ value of ``public()`` gives an entirely explicit spelling that requires no speci Here the binding is a plain assignment, visible to every tool that parses Python. This form is a transition aid rather than the recommended spelling, and it should not be needed for long. -The conclusion is that for the data and type alias use cases, which are the places a decorator +The conclusion is that the data and type alias use cases, which are the places a decorator genuinely cannot be utilized, do not require new syntax at all. They require a function call that tools can learn to read, alleviating the need for a dedicated, new ``export`` keyword. @@ -314,15 +341,15 @@ and it applies with equal force here: now." No. This PEP is not urgent, and it does not claim to be. Nothing about module name visibility, or -the exported, public API of a module is. But urgency is the wrong test to apply to this particular -proposal, for three reasons. +about a module's exported public API, is urgent. But urgency is the wrong test to apply to this +particular proposal, for three reasons. **The feature is not new.** This PEP does not ask Python to adopt an untried idea; ``atpublic`` has implemented these exact semantics since 2016. The question is not "should Python have this?" since users who want it already have it, but "should having it cost a third party dependency?" A decade of production use is the opposite of rushing. It has already surfaced and settled the corner cases, syntax, and semantics a fresh design would have to guess at: that only module-level objects can be -decorated, what to do about a non-list ``__all__``, and what the functional form should return. +decorated, what to do about a non-list ``__all__``, and what the function call form should return. **The cost of being wrong is low.** The urgency argument has the most weight against changes that cannot be walked back. Syntax is permanent: a soft keyword constrains the grammar forever, must be @@ -348,7 +375,7 @@ Import time performance When this idea was informally floated with core developers some years ago, before either :pep:`842` or PEP 843 existed, the objection raised was not the design but the cost weighed against its -utility: decorators runs at import time, once per decorated name, and CPython's startup time is a +utility: a decorator runs at import time, once per decorated name, and CPython's startup time is a closely watched number. The concern is legitimate and deserves a direct answer. **The work per call is small and bounded.** ``public()`` in decorator form reads the decorated @@ -379,8 +406,8 @@ That trade-off does not exist in CPython. A builtin is compiled as part of the fast implementation is simply *the* implementation, with no wheel platform support matrix, no fallback path, and no optional extra. Moreover, a C implementation inside the interpreter can do less work than any third party one: the decorator form can access the calling frame's globals -directly rather than going through ``__module__`` and a :data:`sys.modules` lookup, and the function -call form needs no Python-level stack inspection. +directly, rather than the ``__module__`` plus :data:`sys.modules` lookup a pure Python +implementation requires, and the function call form needs no Python-level stack inspection. The argument is therefore somewhat the reverse of the original objection. The performance concern is a reason to put ``public()`` in builtins where it can be made fast, rather than a reason to leave @@ -396,8 +423,8 @@ it on PyPI, where it cannot. Relationship to PEP 842 and PEP 843 =================================== -TL;DR: :pep:`842` proposes adding an ``export`` keyword and a new module global ``__export__`` -variable. PEP 843 proposes adding a ``from ... export ...`` form. +TL;DR: :pep:`842`, in its current revision, proposes adding an ``export`` keyword and a new module +global ``__export__`` variable. PEP 843 proposes adding a ``from ... export ...`` form. Two problems, not one @@ -409,14 +436,16 @@ Discussion of module visibility addresses two separable problems: where the object so named is defined, so the declaration drifts out of sync with the implementation. This is a problem about *where you add the declaration*. -2. **Runtime consequences.** ``__all__`` governs only ``from spam import *``. It has no effect on - attribute access, :func:`dir`, :func:`help`, or autocompletion, so a name that is intended to be - kept private is indistinguishable from public names to these patterns of module introspection. - This is a problem about *what the declaration does*. +2. **Runtime consequences.** ``__all__`` declares the public API, but the only place that + declaration is enforced is ``from spam import *``. It has no effect on attribute access, + :func:`dir`, :func:`help`, or autocompletion, so a name that is intended to be kept private is + indistinguishable from public names to these patterns of module introspection. This is a + problem about *what the declaration does*. -This PEP addresses only the first. It takes the position that the first problem is the more urgent -and the more broadly applicable of the two, that it can be solved without new syntax and without a -new variable, and that solving it does not commit Python to any particular answer to the second. +This PEP addresses only the first. It takes the position that the first problem is the more +pressing and the more broadly applicable of the two, that it can be solved without new syntax and +without a new variable, and that solving it does not commit Python to any particular answer to the +second. .. _pep-844-why-all: @@ -449,7 +478,7 @@ maintained by hand has it, and no variable maintained at the definition site doe The first half of that quote is the argument this PEP is built on, and the second half names the decorator as the ergonomic answer for classes and functions. The remaining question -- what to do -about data and type aliases, where there is nothing to decorate -- is addressed by PEP 844 in +about data and type aliases, where there is nothing to decorate -- is addressed in :ref:`pep-844-static-analysis`. The second reason is that ``__all__`` is not always exhaustive in practice. A module may @@ -485,14 +514,16 @@ Why PEP 843 is a good companion ------------------------------- This PEP does **not** solve the DRY problem for re-exports, and cannot do so gracefully. A "hub -module" that pulls names out of private submodules must currently write each name twice: +module" that pulls names out of private submodules must currently write each name three times: .. code-block:: python from ._core import Widget public(Widget=Widget) -Notice how often ``PublicThing`` is named here. That's a big violation of DRY! +``Widget`` is named once to import it, and twice more to export it. That's a big violation of DRY! +Hand-maintaining ``__all__`` would name it only twice, so for re-exports specifically, ``public()`` +is not merely unhelpful, it is a step backwards. The decorator form of ``@public`` is unavailable here because there is nothing to decorate, and the function call form of ``public()`` requires naming the binding explicitly. This is exactly the gap @@ -502,7 +533,7 @@ can. The two proposals therefore partition the problem cleanly, and provide excellent synergy: * ``public()`` and ``private()`` handle the names a module **defines**. -* ``from export `` handles imported names a module *passes through**. +* ``from export `` handles the names a module **passes through**. Both populate ``__all__``. Neither requires the other, and neither requires new runtime semantics for the result. @@ -525,6 +556,11 @@ Code that imports ``public`` and ``private`` from the ``atpublic`` package will unchanged (as long as the semantics continue to match), since an explicit import shadows the builtin. +Code that already uses ``public`` or ``private`` as a variable or parameter name will begin to trip +linters that flag shadowed builtins, such as ``flake8-builtins`` and the equivalent ``ruff`` rule. +This is a diagnostic change rather than a behavioral one, and the same has been true of every +builtin added to Python. How much existing code this affects has not been measured. + Modules using these builtins will not run on Python 3.15 and earlier without either a dependency on ``atpublic`` or a compatibility shim. @@ -546,13 +582,27 @@ The rule to teach is a single sentence: decorate a name with ``@public`` if user meant to use it, and don't decorate it (or decorate it with ``@private``, to say so explicitly) if they aren't. +Constants and other names that cannot be decorated use the function call form, which both binds the +name and marks it public: + +.. code-block:: python + + public(SEVEN=7) + +This replaces the assignment rather than accompanying it. Writing ``SEVEN = 7`` as well would +define the name twice, which is the repetition these builtins exist to remove. + +Adoption can be incremental. A module with a hand-written ``__all__`` can start decorating +definitions without removing it, because names already listed are not added twice, and the two +styles can coexist indefinitely. + Reference Implementation ======================== The `atpublic `__ package, available on PyPI and maintained since -2016, implements the proposed semantics in pure Python. Its source is at -https://gitlab.com/flufl/public. +2016, implements the proposed semantics in pure Python. Its `source repository +`__ is hosted on GitLab. A CPython implementation has not yet been written. @@ -560,6 +610,11 @@ For a time, ``atpublic`` also included a C implementation of ``public()``, which faster than the pure Python one. It was dropped for packaging reasons that do not apply to a builtin. See :ref:`pep-844-performance`. +One divergence is worth noting. ``atpublic`` 7.0.0 and earlier create ``__all__`` in the +``@private`` case, contrary to the specification above. This was identified as a bug while drafting +this PEP, and will be corrected in ``atpublic`` 8.0.0, which is in pre-release at the time of this +writing. + Rejected Ideas ============== @@ -567,14 +622,14 @@ Rejected Ideas New ``export`` syntax instead of decorators ------------------------------------------- -:pep:`842` proposes an ``export`` soft keyword covering the same ground as this PEP -- ``export -def``, ``export class``, ``export NAME = value``. Its `Rejected Ideas +:pep:`842`, in its current revision, proposes an ``export`` soft keyword covering the same ground as +this PEP -- ``export def``, ``export class``, ``export NAME = value``. Its `Rejected Ideas `_ section considers builtin ``public`` and ``private`` decorators, describes them as the author's next preferred alternative to syntax, and rejects them on the grounds that "there's no easy way to export simple variables without duplicating the name." -That objection doesn't fully apply to the design proposed here. The keyword call form exists +That objection doesn't fully apply to the design proposed here. The function call form exists precisely for the undecoratable cases, and writes the name exactly once: .. code-block:: python @@ -606,13 +661,13 @@ See :ref:`pep-844-why-all`. Leave it on PyPI ---------------- -Leaving ``atpublic`` on PyPI is certain a status quo decision that leaves the solution in the same +Leaving ``atpublic`` on PyPI is certainly a status quo decision that leaves the solution in the same state it's currently in. Users who want to opt into this functionality can simply add that library as a dependency and import the functions (or use the ``pip install atpublic[install]`` extra to populate builtins). However, if this *is* a problem worth solving now, then leaving this in a third-party module on PyPI -doesn't serve our users adequately. The need to include a dependency and an explicit important may +doesn't serve our users adequately. The need to include a dependency and an explicit import may be just enough of a hurdle (albeit small) to stop widespread use of it. Adding it to builtins gives this pattern a promotional endorsement that will gain in popularity. @@ -636,8 +691,8 @@ Open Issues function, also be included? This is deferred for now; a heuristic is a harder case to make for a builtin than the two explicit declarations are, and is less essential for improving module visibility ergonomics. -* Should ``private()`` support a keyword call form, for symmetry? ``atpublic`` does not provide one - and no need for it has ever been demonstrated or requested. +* Should ``private()`` support a function call form, for symmetry? ``atpublic`` does not provide + one and no need for it has ever been demonstrated or requested. * Should the standard library itself adopt these decorators, and if so on what schedule? This question is entangled with :ref:`pep-844-performance` and should be settled with startup measurements in hand. Also, as with all new capabilities (such as lazy imports), Python's policy @@ -659,6 +714,7 @@ Change History TBD + Copyright ========= From 33d30ea55f3b2d927229332dbc4cb945bc091948 Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Thu, 6 Aug 2026 11:40:42 -0700 Subject: [PATCH 03/12] Update peps/pep-0844.rst Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- peps/pep-0844.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/peps/pep-0844.rst b/peps/pep-0844.rst index 257fd353674..93aff1ce9c0 100644 --- a/peps/pep-0844.rst +++ b/peps/pep-0844.rst @@ -49,7 +49,7 @@ This PEP is an adjunct to :pep:`842` and PEP 843; see `Relationship to PEP 842 a Motivation ========== -The `module global variable `_ +The :attr:`module global variable ` ``__all__`` is the mechanism Python currently defines for declaring a module's public names. However, ``__all__`` suffers from a well known problem: it is typically defined as a separate list often far from the objects whose names are contained in it. An object defined at one point in the From 1d0706580d599094d8216b86b3deb1091d921688 Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Thu, 6 Aug 2026 11:40:57 -0700 Subject: [PATCH 04/12] Update peps/pep-0844.rst Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- peps/pep-0844.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/peps/pep-0844.rst b/peps/pep-0844.rst index 93aff1ce9c0..2fc97950aa7 100644 --- a/peps/pep-0844.rst +++ b/peps/pep-0844.rst @@ -40,7 +40,7 @@ For example: >>> spam.__all__ ['Public', 'SEVEN'] -The proposed semantics are those of the third party `atpublic +The proposed semantics are those of the third-party `atpublic `__ package, which has provided this functionality since 2016. This PEP is an adjunct to :pep:`842` and PEP 843; see `Relationship to PEP 842 and PEP 843`_. From f0425a7f4dcd444df4283c0b34191c9f4e0eae14 Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Thu, 6 Aug 2026 11:41:04 -0700 Subject: [PATCH 05/12] Update peps/pep-0844.rst Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- peps/pep-0844.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/peps/pep-0844.rst b/peps/pep-0844.rst index 2fc97950aa7..45f5d187d21 100644 --- a/peps/pep-0844.rst +++ b/peps/pep-0844.rst @@ -75,7 +75,7 @@ answer <842#prefixed-names-aren-t-necessarily-a-great-solution>`. The pattern proposed here -- declaring visibility at the definition site with a decorator -- is not new or speculative. The ``atpublic`` package on PyPI has implemented it for a decade, and it is already depended on by a number of projects. What this PEP proposes is that the pattern is common -enough, and useful enough, to be spelled without a third party dependency. Thus it proposes to add +enough, and useful enough, to be spelled without a third-party dependency. Thus it proposes to add ``atpublic``'s ``public()`` and ``private()`` functions to the builtins. From 425e9ecbfa9f2c184d9ac6962cfa3921b5923a83 Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Thu, 6 Aug 2026 11:41:23 -0700 Subject: [PATCH 06/12] Update peps/pep-0844.rst Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- peps/pep-0844.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/peps/pep-0844.rst b/peps/pep-0844.rst index 45f5d187d21..c8ef59ef375 100644 --- a/peps/pep-0844.rst +++ b/peps/pep-0844.rst @@ -243,7 +243,7 @@ Why builtins? The declaration of a module's public interface is a fundamental and often requested property, especially as code bases grow. Many use cases have been identified in discussions, and different approaches have been developed in different libraries and applications. Enough experience has been -gained over the decade of ``atpublic``'s existence that requiring a third party dependency (or an +gained over the decade of ``atpublic``'s existence that requiring a third-party dependency (or an import at the top of every module) to spell something this fundamental is friction that discourages its use. It is also awkward in exactly the places where it matters most: the standard library itself, and small single file modules. From 05b4705b232a59eea7f602f8259db3bbd49a04ec Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Thu, 6 Aug 2026 11:41:44 -0700 Subject: [PATCH 07/12] Update peps/pep-0844.rst Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- peps/pep-0844.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/peps/pep-0844.rst b/peps/pep-0844.rst index c8ef59ef375..54839c6f562 100644 --- a/peps/pep-0844.rst +++ b/peps/pep-0844.rst @@ -346,7 +346,7 @@ particular proposal, for three reasons. **The feature is not new.** This PEP does not ask Python to adopt an untried idea; ``atpublic`` has implemented these exact semantics since 2016. The question is not "should Python have this?" since -users who want it already have it, but "should having it cost a third party dependency?" A decade +users who want it already have it, but "should having it cost a third-party dependency?" A decade of production use is the opposite of rushing. It has already surfaced and settled the corner cases, syntax, and semantics a fresh design would have to guess at: that only module-level objects can be decorated, what to do about a non-list ``__all__``, and what the function call form should return. From 042287c25f30460c19b02c674d4ceedd7cce4929 Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Thu, 6 Aug 2026 11:41:52 -0700 Subject: [PATCH 08/12] Update peps/pep-0844.rst Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- peps/pep-0844.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/peps/pep-0844.rst b/peps/pep-0844.rst index 54839c6f562..94dcd68f378 100644 --- a/peps/pep-0844.rst +++ b/peps/pep-0844.rst @@ -405,7 +405,7 @@ burden borne entirely so that the pure Python fallback could be avoided. That trade-off does not exist in CPython. A builtin is compiled as part of the interpreter, so the fast implementation is simply *the* implementation, with no wheel platform support matrix, no fallback path, and no optional extra. Moreover, a C implementation inside the interpreter can do -less work than any third party one: the decorator form can access the calling frame's globals +less work than any third-party one: the decorator form can access the calling frame's globals directly, rather than the ``__module__`` plus :data:`sys.modules` lookup a pure Python implementation requires, and the function call form needs no Python-level stack inspection. From 2e658eb67bf77af22c84915e231f18d217f4e8ff Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Thu, 6 Aug 2026 11:42:36 -0700 Subject: [PATCH 09/12] Update peps/pep-0844.rst Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- peps/pep-0844.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/peps/pep-0844.rst b/peps/pep-0844.rst index 94dcd68f378..6f096aa789a 100644 --- a/peps/pep-0844.rst +++ b/peps/pep-0844.rst @@ -624,7 +624,7 @@ New ``export`` syntax instead of decorators :pep:`842`, in its current revision, proposes an ``export`` soft keyword covering the same ground as this PEP -- ``export def``, ``export class``, ``export NAME = value``. Its `Rejected Ideas -`_ section +:pep:`Rejected Ideas <842#rejected-ideas>` section considers builtin ``public`` and ``private`` decorators, describes them as the author's next preferred alternative to syntax, and rejects them on the grounds that "there's no easy way to export simple variables without duplicating the name." From a904bd9c28616a916f618898aa4724784916038e Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Thu, 6 Aug 2026 11:43:59 -0700 Subject: [PATCH 10/12] Add myself to CODEOWNERS for 844 --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 27dda896edc..b3e64cd0c5f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -715,6 +715,7 @@ peps/pep-0837.rst @serhiy-storchaka peps/pep-0838.rst @AlexWaygood peps/pep-0840.rst @jeremyhylton @gvanrossum peps/pep-0842.rst @ZeroIntensity +peps/pep-0844.rst @warsaw # ... peps/pep-2026.rst @hugovk # ... From af1a596fa5b7f2a3b86cd56d773e881db1a788a3 Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Thu, 6 Aug 2026 12:37:19 -0700 Subject: [PATCH 11/12] Update peps/pep-0844.rst Co-authored-by: Peter Bierma --- peps/pep-0844.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/peps/pep-0844.rst b/peps/pep-0844.rst index 6f096aa789a..84ca27f61fe 100644 --- a/peps/pep-0844.rst +++ b/peps/pep-0844.rst @@ -70,7 +70,7 @@ Nothing keeps the two in sync, leading to these problems: The convention of prefixing private names with an underscore addresses a related but different problem, and :pep:`842` describes at length why :pep:`prefixing is not by itself a sufficient -answer <842#prefixed-names-aren-t-necessarily-a-great-solution>`. +answer `. The pattern proposed here -- declaring visibility at the definition site with a decorator -- is not new or speculative. The ``atpublic`` package on PyPI has implemented it for a decade, and it is From eb77ac7e62f9cf2a25896954ef8645a6bb02dada Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Thu, 6 Aug 2026 17:33:51 -0700 Subject: [PATCH 12/12] Formatting, spelling, and other fixes --- peps/pep-0844.rst | 59 ++++++++++++++++++++++------------------------- 1 file changed, 28 insertions(+), 31 deletions(-) diff --git a/peps/pep-0844.rst b/peps/pep-0844.rst index 84ca27f61fe..69a4561d121 100644 --- a/peps/pep-0844.rst +++ b/peps/pep-0844.rst @@ -49,12 +49,11 @@ This PEP is an adjunct to :pep:`842` and PEP 843; see `Relationship to PEP 842 a Motivation ========== -The :attr:`module global variable ` -``__all__`` is the mechanism Python currently defines for declaring a module's public names. -However, ``__all__`` suffers from a well known problem: it is typically defined as a separate list -often far from the objects whose names are contained in it. An object defined at one point in the -file is repeated as a string literal in an ``__all__`` list somewhere else, usually at the top of -the file. +The :attr:`module global variable ` ``__all__`` is the mechanism Python currently +defines for declaring a module's public names. However, ``__all__`` suffers from a well known +problem: it is typically defined as a separate list often far from the objects whose names are +contained in it. An object defined at one point in the file is repeated as a string literal in an +``__all__`` list somewhere else, usually at the top of the file. Nothing keeps the two in sync, leading to these problems: @@ -69,7 +68,7 @@ Nothing keeps the two in sync, leading to these problems: "is this name public?" The convention of prefixing private names with an underscore addresses a related but different -problem, and :pep:`842` describes at length why :pep:`prefixing is not by itself a sufficient +problem, and :pep:`842` describes at length why :ref:`prefixing is not by itself a sufficient answer `. The pattern proposed here -- declaring visibility at the definition site with a decorator -- is not @@ -226,7 +225,7 @@ Restrictions ------------ Note that because ``@public`` is primarily used to keep the ``__all__`` module global in sync, only -module level objects may be decorated. Decorating a method inside a class body is not supported, +module-level objects may be decorated. Decorating a method inside a class body is not supported, since ``__all__`` documents module contents, not class contents. Because ``__all__`` must be mutable for these functions to append to it, a module that assigns @@ -354,7 +353,7 @@ decorated, what to do about a non-list ``__all__``, and what the function call f **The cost of being wrong is low.** The urgency argument has the most weight against changes that cannot be walked back. Syntax is permanent: a soft keyword constrains the grammar forever, must be taught to every future Python programmer, and is unavailable to any module supporting an older -interpreter. A new module level variable with runtime consequences changes the observable behavior +interpreter. A new module-level variable with runtime consequences changes the observable behavior of code without warning. A builtin function is the cheapest thing in this design space on both counts: it is inert until called, it changes nothing about modules that ignore it, and if it proves to be a mistake it can be deprecated in the ordinary way without touching the grammar. @@ -398,9 +397,9 @@ real difference, but it is a constant factor on an already small constant, not a **A C implementation is feasible and fast.** This is the point on which a builtin is strictly better positioned than the third-party package. ``atpublic`` shipped a C implementation of ``public()`` for a time, and it was substantially faster than the pure Python version. It was ultimately -dropped, not because it did not work, but because requiring a compiled extension module in a third -party package is a significant packaging and installation burden for a library this small -- a -burden borne entirely so that the pure Python fallback could be avoided. +dropped, not because it did not work, but because requiring a compiled extension module in a +third-party package is a significant packaging and installation burden for a library this small +-- a burden borne entirely so that the pure Python fallback could be avoided. That trade-off does not exist in CPython. A builtin is compiled as part of the interpreter, so the fast implementation is simply *the* implementation, with no wheel platform support matrix, no @@ -423,8 +422,8 @@ it on PyPI, where it cannot. Relationship to PEP 842 and PEP 843 =================================== -TL;DR: :pep:`842`, in its current revision, proposes adding an ``export`` keyword and a new module -global ``__export__`` variable. PEP 843 proposes adding a ``from ... export ...`` form. +In brief: :pep:`842`, in its current revision, proposes adding an ``export`` keyword and a new +module global ``__export__`` variable. PEP 843 proposes adding a ``from ... export ...`` form. Two problems, not one @@ -457,9 +456,9 @@ Why ``__all__`` and not ``__export__`` :pep:`842` gives two reasons why ``__all__`` is inadequate. The first is that ``__all__`` drifts out of sync with the module. That is true, and it is precisely the problem ``atpublic`` and this -PEP solves. However a *new list of string literals in the same place in the file* does not directly -solve this problem. :pep:`842`'s own revision history concedes the point, quoting `Guido van Rossum -`__ on the original ``__export__``-only design: +PEP solves. However a *new list of string literals in the same distant part of the file* does not +directly solve this problem. :pep:`842`'s own revision history concedes the point, quoting `Guido +van Rossum `__ on the original ``__export__``-only design: But the ergonomics are similar to those of ``__all__``, and those are bad. It's too easy to forget to add (or remove!) something to @@ -549,8 +548,8 @@ Backwards Compatibility ======================= Adding names to :mod:`builtins` shadows nothing, but it does mean that modules which define their -own module level ``public`` or ``private`` names will shadow the builtins instead. This is the same -situation as any other builtin (``id``, ``type``, ``list``), and is well understood. +own module-level ``public`` or ``private`` names will shadow the builtins instead. This is the +same situation as any other builtin (``id``, ``type``, ``list``), and is well understood. Code that imports ``public`` and ``private`` from the ``atpublic`` package will continue to work unchanged (as long as the semantics continue to match), since an explicit import shadows the @@ -623,11 +622,10 @@ New ``export`` syntax instead of decorators ------------------------------------------- :pep:`842`, in its current revision, proposes an ``export`` soft keyword covering the same ground as -this PEP -- ``export def``, ``export class``, ``export NAME = value``. Its `Rejected Ideas -:pep:`Rejected Ideas <842#rejected-ideas>` section -considers builtin ``public`` and ``private`` decorators, describes them as the author's next -preferred alternative to syntax, and rejects them on the grounds that "there's no easy way to export -simple variables without duplicating the name." +this PEP -- ``export def``, ``export class``, ``export NAME = value``. Its +:pep:`Rejected Ideas <842#rejected-ideas>` section considers builtin ``public`` and ``private`` +decorators, describes them as the author's next preferred alternative to syntax, and rejects them on +the grounds that "there's no easy way to export simple variables without duplicating the name." That objection doesn't fully apply to the design proposed here. The function call form exists precisely for the undecoratable cases, and writes the name exactly once: @@ -636,8 +634,8 @@ precisely for the undecoratable cases, and writes the name exactly once: public(SEVEN=7) -is the whole declaration. The name ``SEVEN`` is bound in the module globals, assigned the integer 7 -value, and appended to ``__all__``. There is no separate assignment to keep in sync. Compare +is the whole declaration. The name ``SEVEN`` is bound to ``7`` in the module globals, and +``"SEVEN"`` is appended to ``__all__``. There is no separate assignment to keep in sync. Compare ``export SEVEN = 7``: the two spellings carry the same information, cost roughly the same keystrokes, and differ only in that one of them requires a grammar change. @@ -661,10 +659,9 @@ See :ref:`pep-844-why-all`. Leave it on PyPI ---------------- -Leaving ``atpublic`` on PyPI is certainly a status quo decision that leaves the solution in the same -state it's currently in. Users who want to opt into this functionality can simply add that library -as a dependency and import the functions (or use the ``pip install atpublic[install]`` extra to -populate builtins). +Leaving ``atpublic`` on PyPI is the status quo option. Users who want to opt into this +functionality can simply add that library as a dependency and import the functions (or use the +``pip install atpublic[install]`` extra to populate builtins). However, if this *is* a problem worth solving now, then leaving this in a third-party module on PyPI doesn't serve our users adequately. The need to include a dependency and an explicit import may @@ -699,7 +696,7 @@ Open Issues is generally not to wholesale update the stdlib to embrace the new functionality. These new functions can be utilized opportunistically in modules where the most benefit can be gained, or when a module undergoes substantial rewrite. -* Import time benchmarks for the C implementation are outstanding. +* Import time benchmarks for a C implementation are outstanding. Acknowledgements