From 6b7919a5df6b4c054d7877f8eab190d4ddefc19d Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Fri, 21 Aug 2026 18:57:00 -0700 Subject: [PATCH 1/7] Updates * Update references to :pep:`842` (since withdrawn) and :pep:`843` (since published). * Synchronize this PEP's proposed semantics with ``atpublic``'s 8.0.0 update. --- peps/pep-0844.rst | 384 +++++++++++++++++++++++++++++++--------------- 1 file changed, 258 insertions(+), 126 deletions(-) diff --git a/peps/pep-0844.rst b/peps/pep-0844.rst index 6794a43751a..772ace31bc4 100644 --- a/peps/pep-0844.rst +++ b/peps/pep-0844.rst @@ -16,14 +16,17 @@ This PEP proposes adding two new builtin functions, ``public()`` and ``private() the public interface of a module by keeping its ``__all__`` synchronized with the names actually defined to be public 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. +where the name is defined. Both additionally have a single argument call form for names that are +bound by other means (such as ``from ... import`` statements), and ``public()`` has a keyword +argument form for names that cannot be decorated, such as constants. For example: .. code-block:: python # spam.py + from strings import Bass + @public class Public: ... @@ -32,18 +35,20 @@ For example: class Private: ... - public(SEVEN=7) + public(Bass) + public(TEMPO=120) .. code-block:: pycon >>> import spam >>> spam.__all__ - ['Public', 'SEVEN'] + ['Public', 'Bass', 'TEMPO'] 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`_. +This PEP is an adjunct to :pep:`843`, and to the withdrawn :pep:`842`; see `Relationship to PEP 842 +and PEP 843`_. Motivation @@ -144,51 +149,106 @@ matter of naming convention and documentation. ``public()`` ------------ -``public()`` has two call forms. The decorator form (``@public``) is the most common use. +``public()`` has three 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 in which ``public()`` is called, and returns the -object unchanged: +**Decorator form.** When called with a single positional argument that has a ``__name__`` attribute +(such as a function or a class), ``public()`` appends that object's ``__name__`` to the ``__all__`` +of the module in which ``public()`` is called, and returns the object unchanged: .. code-block:: python @public - def foo(): + def tune(): ... @public - class Bar: + class Cello: ... - # __all__ == ['foo', 'Bar'] + # __all__ == ['tune', 'Cello'] 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__``: +**Single argument form.** The same call written without the ``@`` appends to ``__all__`` a name that +is already bound in the module's globals, such as a name bound by import from another module: .. code-block:: python - public(SEVEN=7) - public(a_bar=Bar()) - public(ONE=1, TWO=2) + from strings import Bass + from reeds import Harmonica as Harp + from woodwinds import piccolo + + public(Bass) + public(Harp) + public(piccolo) + + # __all__ == ['Bass', 'Harp', 'piccolo'] + +The name appended is the one the object is bound to *in the calling module's globals*, not +necessarily the one it was defined with. So ``public(Harp)`` appends ``'Harp'``. Modules and +submodules resolve the same way, which is how a package exports a submodule. The argument is +returned unchanged. + +When an object is bound to more than one name in the calling module, the name it was defined with +wins; by the time ``public()`` runs, nothing distinguishes the two bindings: + +.. code-block:: python + + class Fiddle: + ... + + Violin = Fiddle + + public(Violin) + + # __all__ == ['Fiddle'] + +To export an alias specifically, use the keyword argument form below. -The value of a single keyword argument is returned; for multiple keyword arguments, a tuple of the -values is returned in order: +The single argument can also be a string, which is appended as given: .. code-block:: python - a, b, c = public(a=3, b=2, c=1) - d = public(d=9) + public('Tuba') + +The string must be a valid Python identifier and must not be a reserved word, or :exc:`ValueError` +is raised; nothing can ever be bound to a reserved word, so such an entry in ``__all__`` would be +guaranteed to name something that can never exist. Soft keywords such as ``match`` and ``type`` +are ordinary names and are accepted. Nothing else checks the string against the module's contents, +so the name need not be bound, or even exist. This is the escape hatch for names that do not appear +in the source, such as bindings made dynamically or re-exports guarded by +``try``/``except ImportError``. It should be a last resort since string literals are precisely the +kind of thing that goes stale. + +If no name can be inferred from the argument -- such as for a constant or an instance, neither of +which has a ``__name__`` -- :exc:`TypeError` is raised, with an error message referring to the +keyword argument form. + +**Keyword argument form.** Names which can be neither decorated nor inferred, 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(TEMPO=120) + public(a_cello=Cello()) + public(Violin=Fiddle) + public(ROOT=1, FIFTH=5) + +When used with a single keyword argument, the value is returned. For multiple keyword arguments a +tuple of the values is returned in order: + +.. code-block:: python + + second, third, seventh = public(second=2, third=3, seventh=7) + ninth = public(ninth=9) 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 +:class:`list` before appending. If ``__all__`` exists but is not a list, :exc:`TypeError` is 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. @@ -196,21 +256,40 @@ idempotent, so a name that already appears in ``__all__`` is not added a second ``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: +``private()`` is the dual of ``public()``'s decorator and single argument forms. 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 argument is returned unchanged: .. code-block:: python + import argparse + @private def helper(): ... + private(argparse) + +The single argument form is how a module keeps an imported name it uses internally, such as +``argparse`` above, out of an ``__all__`` that something else in the module has created. + +Names are resolved exactly as they are for ``public()``: an object with a ``__name__``, a module, or +a submodule resolves to the name it is bound to in the calling module's globals, while a string must +be a valid Python identifier that isn't a reserved word and is otherwise taken as given. The same +:exc:`TypeError` and :exc:`ValueError` conditions apply. + +``private()`` has no keyword argument form. Binding a name and declaring it private in a single +call would be a contradiction: the keyword form of ``public()`` exists to introduce a name into the +module globals, and for ``private()`` there is nothing to remove from ``__all__`` that the call +itself just created. + 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. +document the author's intent at the point of definition. The argument is still resolved in that +case, and the result discarded, so that an argument no name can be inferred from is rejected +whether or not the module has an ``__all__`` yet; otherwise a bad argument would sit unnoticed +until something else in the module created one. If ``__all__`` does exist it must be a list, or +:exc:`TypeError` is raised, and the 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 @@ -222,12 +301,6 @@ names is the job of ``@public``: as soon as any name in the module is marked pub 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 form, as no valid use case for it has been - identified or requested by users of the ``atpublic`` package. See `Open Issues`_ for further - discussion. - Restrictions ------------ @@ -238,9 +311,10 @@ since ``__all__`` documents module contents, not class contents. Neither function inspects the scope it is called from, so this misuse is not currently diagnosed. A decorator applied to a method appends the method's name to the enclosing *module's* ``__all__``, -and a function call form used in a class body binds its keywords in the module globals rather than -in the class body. Neither outcome is likely to be what the author intended. Whether these cases -should raise an exception instead is an `Open Issues`_ question. +a single argument call in a class body does the same, and the keyword argument form binds its +keywords in the module globals rather than in the class body. None of these outcomes is likely to +be what the author intended. Whether these cases should raise an exception instead is an `Open +Issues`_ question. 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 @@ -289,33 +363,39 @@ is to say, at the point of definition, that the name is deliberately not public. .. _pep-844-static-analysis: -Static analysis of the function call form ------------------------------------------ +Static analysis of the function call forms +------------------------------------------ -The strongest objection to this proposal concerns the function call form, and it is worth stating -explicitly. Given: +The strongest objection to this proposal concerns the keyword argument form, and it is worth +stating explicitly. Given: .. code-block:: python - public(SEVEN=7) + public(TEMPO=120) -``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 +``TEMPO`` is bound in the module's globals by a function that reaches into its caller's frame. +Nothing about that binding is visible to static analyzers. 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``, 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. +``TEMPO`` as undefined at every use site. A soft keyword like ``export TEMPO = 120``, as :pep:`842` +proposed, 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. + +The objection is specific to the keyword argument form. The single argument form binds nothing: +``public(Bass)`` is an ordinary reference to a name the module has already bound, so no tool can be +misled into thinking ``Bass`` is undefined. All a checker has to learn there is that the call +contributes a name to ``__all__`` -- exactly what it already learns from ``__all__.append('Bass')``, +and with the same information available in the source. -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. +This could easily be rectified by future modifications to linting tools, so that they explicitly +recognize the keyword argument 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 -that these tools can easily perform. +that ``public(TEMPO=120)`` binds ``TEMPO`` and appends ``"TEMPO"`` to ``__all__`` is a simple +analysis that these tools could easily perform. There is direct precedent. Static analyzers already model ``__all__`` mutation beyond simple assignment, including ``__all__ += [...]`` and ``__all__.append(...)``, precisely because real code @@ -330,7 +410,7 @@ value of ``public()`` gives an entirely explicit spelling that requires no speci .. code-block:: python - SEVEN = public(SEVEN=7) + TEMPO = public(TEMPO=120) 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. @@ -362,7 +442,8 @@ implemented these exact semantics since 2016. The question is not "should Pytho 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. +decorated, what to do about a non-list ``__all__``, which module's ``__all__`` a re-exported name +belongs in and which of its names to use, and what each 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 @@ -372,13 +453,13 @@ of code without warning. A builtin function is the cheapest thing in this desig 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. +**The sequencing matters more than the timing.** Three proposals in this cycle addressed the same +problem space, two of them asking for new syntax. :pep:`842` has since been withdrawn, but +:pep:`843` remains, and the point is unchanged: 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 +may never be added, regardless of whether they were the better answer. That asymmetry is the reason +to consider this PEP now rather than later. .. _pep-844-performance: @@ -387,12 +468,12 @@ 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 +or :pep:`843` existed, the objection raised was not the design but the cost weighed against its 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 -object's ``__name__``, obtains the defining module's globals, creates ``__all__`` as an empty list +object's ``__name__``, obtains the calling 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. @@ -403,7 +484,7 @@ or not it participates. A module that does call it pays once per *public* name, 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 +:pep:`842`'s ``export`` statement was 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. @@ -418,9 +499,11 @@ third-party package is a significant packaging and installation burden for a lib 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 the ``__module__`` plus :data:`sys.modules` lookup a pure Python -implementation requires, and the function call form needs no Python-level stack inspection. +less work than any third-party one: it has the calling frame in hand and can read its globals +directly, where a pure Python implementation must call :func:`sys._getframe` on every call, as +``atpublic`` (as of version 8.0.0) does in all three forms. The name resolution that the single +argument form performs is the same work either way. What the builtin saves is the frame lookup and +the Python-level call itself. 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 @@ -436,8 +519,15 @@ it on PyPI, where it cannot. Relationship to PEP 842 and PEP 843 =================================== -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. +In brief: :pep:`842` proposed adding an ``export`` keyword and a new module global ``__export__`` +variable, with runtime enforcement of the resulting declaration. :pep:`843` proposes adding a +``from ... export ...`` form for re-exports, which populates ``__all__``. + +:pep:`842` has since been withdrawn. Its author's stated reason is that the proposal grew out of a +need to improve standard library maintenance, and the solution it described "did not align with the +needs of third-party packages." Its material is retained in the comparisons below because the +questions it raised about ``__all__`` outlive it, and because this PEP's design is in part a +response to them. Two problems, not one @@ -466,9 +556,11 @@ second. Why ``__all__`` and not ``__export__`` -------------------------------------- -:pep:`842` proposes a new ``__export__`` variable. This PEP proposes to keep using ``__all__``. +:pep:`842` proposed a new ``__export__`` variable. This PEP proposes to keep using ``__all__``. +The choice between reusing ``__all__`` and introducing a second variable is relevant regardless of +that PEP's withdrawal, so the reasoning is set out here in full. -:pep:`842` gives two reasons why ``__all__`` is inadequate. The first is that ``__all__`` drifts +:pep:`842` gave 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 solve. 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 @@ -505,7 +597,7 @@ that ``__all__`` "should contain the entire public API." A module that withhold ``__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: +What that trade exposes is a real flaw, but a different one from the one :pep:`842` diagnosed: ``__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. @@ -520,28 +612,33 @@ This PEP takes no position on whether unexported-name warnings are desirable. I 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`. +a future proposal along :pep:`842`'s lines. 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 three times: +This PEP narrows the DRY problem for re-exports, but it does not close it and cannot close it +gracefully. A "hub module" that pulls names out of private submodules writes each name twice: .. code-block:: python from ._core import Widget - public(Widget=Widget) - -``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, but 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 -PEP 843 identifies, and its ``from ._core export Widget`` spelling closes it in a way no decorator -can. + public(Widget) + +Manually maintaining ``__all__`` also names ``Widget`` twice, once in the import and once as a +string literal, so the single argument form is an improvement in kind rather than in count: the +second mention comes from the object's ``__name__`` itself, so typos are impossible. The name +survives refactoring, and a checker can see the binding. It is still a second mention on a second +line, one per exported name, and a hub that exports three hundred names carries three hundred of +them. + +``from ._core export Widget`` names it once, in the statement that had to be there anyway. That is +exactly the gap :pep:`843` identifies, and no call form can ergonomically close it +[#import-magic]_. In an import statement, the decorator form has nothing to decorate, the single +argument form needs the name as an argument, and the keyword form needs it on both sides. Aliases +show the same shape -- :pep:`843` writes ``from ._core export Widget as PublicWidget``, where this +PEP needs the import followed by ``public(PublicWidget)``. The two proposals therefore partition the problem cleanly, and provide excellent synergy: @@ -553,9 +650,10 @@ 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`_. + :pep:`843` was published as this PEP was being drafted, and is now in its second round of + discussion. :pep:`842`, which had grown an ``export`` statement of its own overlapping both + this PEP and :pep:`843`, has since been withdrawn. What remains to be settled is therefore the + relationship between this PEP and :pep:`843`; see `Open Issues`_. Backwards Compatibility @@ -591,20 +689,33 @@ 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. +The rule to teach is a single sentence: decorate a class or function definition 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. -Constants and other names that cannot be decorated use the function call form, which both binds the -name and marks it public: +Constants and other names that cannot be decorated use the keyword argument form, which both binds +the name and marks it public: .. code-block:: python - public(SEVEN=7) + public(TEMPO=120) -This replaces the assignment rather than accompanying it. Writing ``SEVEN = 7`` as well would +This replaces the assignment rather than accompanying it. Writing ``TEMPO = 120`` as well would define the name twice, which is the repetition these builtins exist to remove. +Names that arrive by import are declared by passing the object itself, after the import that bound +it: + +.. code-block:: python + + from ._core import Widget + + public(Widget) + +The rule to teach for the two positional spellings is that they are the same call: ``@public`` is +what you write when you are defining the name here, and ``public(name)`` is what you write when it +is already bound. + 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. @@ -615,18 +726,29 @@ Reference Implementation The `atpublic `__ package, available on PyPI and maintained since 2016, implements the proposed semantics in pure Python. Its `source repository -`__ is hosted on GitLab. +`__ is hosted on GitLab. The specification above describes +``atpublic`` 8.0.0, first released as 8.0.0a1 on 21-Aug-2026. -A CPython implementation has not yet been written. +A CPython PR 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`. -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. +Three changes in 8.0.0 are worth calling out, because 7.0.0 and earlier diverge from the +specification above: + +* ``@private`` no longer creates ``__all__`` when the module does not already have one. Leaving an + empty ``__all__`` behind is not the same thing as not adding one, and the difference is observable + in ``from spam import *``. This was identified as a bug while drafting this PEP. +* ``public(thing)`` and ``private(thing)`` used to resolve against + ``sys.modules[thing.__module__]``, the module where ``thing`` was *defined*. For a decorator + those are the same module, but passing an imported object added the name to the wrong module's + ``__all__``. Both functions now always use the globals of the module where the call appears, as + specified above. +* The single argument form is consequently new as a supported spelling in 8.0.0, along with the + string form and the resolution rules given above. Before that, a re-export had to be written + ``public(Widget=Widget)``, which is the spelling earlier drafts of this PEP specified. Rejected Ideas @@ -635,22 +757,23 @@ Rejected Ideas 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 -: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." +Before its withdrawal, :pep:`842` proposed an ``export`` soft keyword covering the same ground as +this PEP, for example ``export def``, ``export class``, ``export NAME = value``. Its :pep:`Rejected +Ideas <842#rejected-ideas>` section considered builtin ``public`` and ``private`` decorators, +described them as the author's next preferred alternative to syntax, and rejected them on the +grounds that "there's no easy way to export simple variables without duplicating the name." The +objection outlives the PEP that raised it, and is answered here. -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: +That objection doesn't fully apply to the design proposed here. The keyword argument form exists +precisely for the cases which can't be decorated, and writes the name exactly once: .. code-block:: python - public(SEVEN=7) + public(TEMPO=120) -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 +is the whole declaration. The name ``TEMPO`` is bound to ``120`` in the module globals, and +``"TEMPO"`` is appended to ``__all__``. There is no separate assignment to keep in sync. Compare +``export TEMPO = 120``: 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 @@ -695,15 +818,14 @@ builtins. Two functions likely aren't worth the cost of a new top-level module. 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 +* How should this PEP and :pep:`843` be reconciled? With :pep:`842` withdrawn, the two remaining + proposals overlap only on re-exports, where this PEP's single argument form and :pep:`843`'s + ``from ... export ...`` statement do the same job with different costs. Whether both are wanted, + and in what order they should be considered, needs to be settled on the discussion threads. +* Should ``populate_all()``, ``atpublic``'s heuristic to infer ``__all__`` from the module's own + definitions, 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 function call form, for symmetry? ``atpublic`` does not provide - one and no need for it has ever been demonstrated or requested. * Should ``public()`` and ``private()`` diagnose being called outside module scope? Neither inspects its calling scope today, so ``@public`` on a method silently adds the method's name to the module's ``__all__``. Raising an exception would be friendlier, at the cost of a scope check @@ -720,14 +842,24 @@ Open Issues 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. +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. + + +Footnotes +========= + +.. [#import-magic] Except for a function call that *also* does the import, but that's even more + magical. Change History ============== -TBD +* 21-Aug-2026 + + * Update references to :pep:`842` (since withdrawn) and :pep:`843` (since published). + * Synchronize this PEP's proposed semantics with ``atpublic``'s 8.0.0 update. Copyright From 14766a5f2ce5c16877c0c5c3ecece1dfdef02918 Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Wed, 26 Aug 2026 09:05:02 -0700 Subject: [PATCH 2/7] Open issue: what to do about the type of __all__ if not a list? --- peps/pep-0844.rst | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/peps/pep-0844.rst b/peps/pep-0844.rst index 772ace31bc4..98d35b47d1d 100644 --- a/peps/pep-0844.rst +++ b/peps/pep-0844.rst @@ -830,6 +830,32 @@ Open Issues inspects its calling scope today, so ``@public`` on a method silently adds the method's name to the module's ``__all__``. Raising an exception would be friendlier, at the cost of a scope check on every call, which bears on :ref:`pep-844-performance`. +* What should ``public()`` and ``private()`` do when ``__all__`` exists but is not a list? The + `Specification`_ requires a list and raises :exc:`TypeError` otherwise, which is stricter than + Python itself. The import system accepts any indexable sequence, so a tuple ``__all__`` is valid + today, while a set is not. Three alternatives have been raised: + + * *Keep the list requirement.* It is the simplest rule to specify and to implement, it makes + ``__all__ = tuple(__all__)`` a genuine freeze after the last declaration (see `Restrictions`_). + + * *Define appending as an operation rather than a type.* ``public()`` would append with the + equivalent of ``__all__ += (name,)``, which extends a list in place and rebinds a tuple, a + :class:`~collections.UserList`, or anything else supporting the addition of a one element tuple, + preserving whatever type the module chose. An object supporting neither raises :exc:`TypeError` + from the operation itself, with no type check needed. There are two consequences. + ``private()`` cannot be symmetric, since Python does not support ``-=`` on tuples, thus still + requiring a mutable ``__all__``; a tuple would therefore accept declarations but not + retractions. A tuple is also copied on every declaration, which is quadratic in the size of the + public API, although an implementation can fast-path an exact list to ``list.append``. + + * *Deprecate the non-list case.* ``public()`` would accept a non-list ``__all__`` indefinitely, + issue a :exc:`DeprecationWarning`, and decline to append the name. Declining to append yields a + silently incomplete public API, reported only through a warning category that is suppressed by + default, and a construct that the import system accepts is not this PEP's to deprecate. + + Whichever is chosen, ``atpublic`` should be synchronized with it, and the `Restrictions`_ note + about freezing ``__all__`` needs to match. :pep:`843`'s desugaring converts ``__all__`` to a list, + so the two proposals should also agree on what ``__all__`` may be. * 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 @@ -856,10 +882,12 @@ Footnotes Change History ============== -* 21-Aug-2026 +* 24-Aug-2026 * Update references to :pep:`842` (since withdrawn) and :pep:`843` (since published). * Synchronize this PEP's proposed semantics with ``atpublic``'s 8.0.0 update. + * Add an `Open Issues`_ entry on what ``public()`` and ``private()`` should do with an ``__all__`` + that is not a list, outlining the alternatives raised on the discussion thread. Copyright From 281243b04cc6040474993bff67079d696584b778 Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Wed, 9 Sep 2026 15:12:19 -0700 Subject: [PATCH 3/7] Mechanical updates --- peps/pep-0844.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/peps/pep-0844.rst b/peps/pep-0844.rst index 98d35b47d1d..e93203dddfb 100644 --- a/peps/pep-0844.rst +++ b/peps/pep-0844.rst @@ -1,12 +1,12 @@ PEP: 844 Title: ``public`` and ``private`` builtins Author: Barry Warsaw -Discussions-To: https://discuss.python.org/t/pep-844-public-and-private-builtins/108515 +Discussions-To: https://discuss.python.org/t/108515 Status: Draft Type: Standards Track Created: 05-Aug-2026 Python-Version: 3.16 -Post-History: `11-Aug-2026 `__ +Post-History: `11-Aug-2026 `__ Abstract @@ -854,8 +854,8 @@ Open Issues default, and a construct that the import system accepts is not this PEP's to deprecate. Whichever is chosen, ``atpublic`` should be synchronized with it, and the `Restrictions`_ note - about freezing ``__all__`` needs to match. :pep:`843`'s desugaring converts ``__all__`` to a list, - so the two proposals should also agree on what ``__all__`` may be. + about freezing ``__all__`` needs to match. :pep:`843`'s de-sugaring converts ``__all__`` to a + list, so the two proposals should also agree on what ``__all__`` may be. * 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 From 4770abd6b1026d2e76ae26108a4b188b20755695 Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Wed, 9 Sep 2026 16:57:45 -0700 Subject: [PATCH 4/7] Clear decision on __all__ must be a list and other clarifications --- peps/pep-0844.rst | 94 ++++++++++++++++++++++++++++++----------------- 1 file changed, 60 insertions(+), 34 deletions(-) diff --git a/peps/pep-0844.rst b/peps/pep-0844.rst index e93203dddfb..0345a8b48f5 100644 --- a/peps/pep-0844.rst +++ b/peps/pep-0844.rst @@ -247,10 +247,11 @@ tuple of the values is returned in order: 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:`TypeError` is -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. +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:`TypeError` is raised; see +:ref:`pep-844-all-must-be-list`. 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()`` @@ -318,7 +319,8 @@ Issues`_ question. 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__)``. +after the last declaration with ``__all__ = tuple(__all__)``. This constrains only those modules +that call ``public()`` or ``private()``; see :ref:`pep-844-all-must-be-list`. Rationale @@ -793,6 +795,56 @@ Add a new ``__export__`` variable See :ref:`pep-844-why-all`. +.. _pep-844-all-must-be-list: + +Accept an ``__all__`` that is not a list +---------------------------------------- + +The `Specification`_ requires ``__all__`` to be a list for ``public()`` and ``private()`` to operate +on it, which is stricter than either the Python language or the CPython implementation. The +language reference only requires it to be "a sequence of strings", and CPython is even looser: +``from spam import *`` iterates over ``__all__`` until it raises :exc:`IndexError`, so a tuple, a +string, and a custom object with a suitable :meth:`~object.__getitem__` are all legal today. + +The restriction that ``__all__`` be a list is deliberate and local to this PEP, and applies only to +modules that call ``public()`` or ``private()``. This PEP does not propose any change to what +``__all__`` may be in either the language reference or CPython implementation. + +Accepting any sequence is not implementable. There is no general way to append to an arbitrary +object that merely supports ``__getitem__``, so "whatever the import system accepts" cannot be the +rule. Any rule broader than *list* is an arbitrary line drawn somewhere short of what the language +permits, and the obvious candidate, *list or tuple*, is unworkable: + +* :class:`tuple` has no ``__iadd__``. While ``__all__ += (name,)`` genuinely extends a list in + place, it only appears to work for a tuple, because the augmented assignment statement falls + back to ``__add__`` and rebinds the name. That is behavior of the statement, not a protocol a + function can require of its argument. + +* ``public()`` could rebind the name in the calling module's globals itself, but then every + declaration copies the whole tuple, which is quadratic in the size of the public API and paid at + import time. See :ref:`pep-844-performance`. + +* ``private()`` could not be made symmetric. Python has no ``-=`` on tuples, so a tuple + ``__all__`` would accept declarations but refuse retractions. + +Officially deprecating the non-list case was also considered and rejected. ``public()`` would +accept a non-list ``__all__`` indefinitely, issue a :exc:`DeprecationWarning`, and decline to append +the name. Declining to append yields a silently incomplete public API, reported only through a +warning category that is suppressed by default. Deprecating a working construct is out of scope for +this PEP. + +The requirement that ``__all__`` be a list incurs minimal costs, and affects only modules opting in +to this PEP. A module that wants an immutable ``__all__`` can still get one by freezing it after +the last declaration with ``__all__ = tuple(__all__)``. Two long-time users of a tuple ``__all__`` +said on the discussion thread that the constraint does not trouble them: `one +`__ concluded, after some weeks of discussion, that every +argument for supporting tuples they had thought of was "baseless or petty", and `another +`__ offered to switch to lists. The "list rule" is also +the easiest one for static analysis to follow, which matters for a proposal whose value depends on +tools recognizing it: ``__all__`` is a list, built by a literal, by ``append``, or by these +declarations. + + Leave it on PyPI ---------------- @@ -830,32 +882,6 @@ Open Issues inspects its calling scope today, so ``@public`` on a method silently adds the method's name to the module's ``__all__``. Raising an exception would be friendlier, at the cost of a scope check on every call, which bears on :ref:`pep-844-performance`. -* What should ``public()`` and ``private()`` do when ``__all__`` exists but is not a list? The - `Specification`_ requires a list and raises :exc:`TypeError` otherwise, which is stricter than - Python itself. The import system accepts any indexable sequence, so a tuple ``__all__`` is valid - today, while a set is not. Three alternatives have been raised: - - * *Keep the list requirement.* It is the simplest rule to specify and to implement, it makes - ``__all__ = tuple(__all__)`` a genuine freeze after the last declaration (see `Restrictions`_). - - * *Define appending as an operation rather than a type.* ``public()`` would append with the - equivalent of ``__all__ += (name,)``, which extends a list in place and rebinds a tuple, a - :class:`~collections.UserList`, or anything else supporting the addition of a one element tuple, - preserving whatever type the module chose. An object supporting neither raises :exc:`TypeError` - from the operation itself, with no type check needed. There are two consequences. - ``private()`` cannot be symmetric, since Python does not support ``-=`` on tuples, thus still - requiring a mutable ``__all__``; a tuple would therefore accept declarations but not - retractions. A tuple is also copied on every declaration, which is quadratic in the size of the - public API, although an implementation can fast-path an exact list to ``list.append``. - - * *Deprecate the non-list case.* ``public()`` would accept a non-list ``__all__`` indefinitely, - issue a :exc:`DeprecationWarning`, and decline to append the name. Declining to append yields a - silently incomplete public API, reported only through a warning category that is suppressed by - default, and a construct that the import system accepts is not this PEP's to deprecate. - - Whichever is chosen, ``atpublic`` should be synchronized with it, and the `Restrictions`_ note - about freezing ``__all__`` needs to match. :pep:`843`'s de-sugaring converts ``__all__`` to a - list, so the two proposals should also agree on what ``__all__`` may be. * 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 @@ -882,12 +908,12 @@ Footnotes Change History ============== -* 24-Aug-2026 +* 09-Sep-2026 * Update references to :pep:`842` (since withdrawn) and :pep:`843` (since published). * Synchronize this PEP's proposed semantics with ``atpublic``'s 8.0.0 update. - * Add an `Open Issues`_ entry on what ``public()`` and ``private()`` should do with an ``__all__`` - that is not a list, outlining the alternatives raised on the discussion thread. + * Reject non-list ``__all__`` outright. The list requirement is kept, and only affects those + modules that opt in to this PEP by calling ``public()`` or ``private()``. Copyright From f994bf049965fc8f561481f79ff9fdabcd3f2e02 Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Wed, 9 Sep 2026 17:22:17 -0700 Subject: [PATCH 5/7] Some additional improvements to the wording --- peps/pep-0844.rst | 58 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 49 insertions(+), 9 deletions(-) diff --git a/peps/pep-0844.rst b/peps/pep-0844.rst index 0345a8b48f5..f45701f7d0d 100644 --- a/peps/pep-0844.rst +++ b/peps/pep-0844.rst @@ -292,6 +292,10 @@ whether or not the module has an ``__all__`` yet; otherwise a bad argument would until something else in the module created one. If ``__all__`` does exist it must be a list, or :exc:`TypeError` is raised, and the name is removed from it if present. +Declarations take effect in the order they execute, and the last one wins. ``public(X)`` followed +later by ``private(X)`` leaves ``X`` out of ``__all__``, while ``private(X)`` followed by +``public(X)`` puts it in. Neither combination is an error. See :ref:`pep-844-why-private`. + ``@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 @@ -348,21 +352,57 @@ Why decorators? 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 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. +The value of these decorators as source code documentation 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-why-private: + +Why ``private()``? +------------------ + +Two objections to ``private()`` were raised repeatedly during discussion: that it is redundant once +``@public`` exists, and that declaring the same name both public and private should be an error +rather than a silent removal from ``__all__``. This PEP keeps ``private()`` and its removal +behavior. + +**Doing nothing mechanically is the point.** In a module that has an ``__all__``, everything not +included explicitly is already excluded, so ``@private`` changes no behavior. That is precisely the +case in which its value as source code documentation is highest. A reader looking at an undecorated +definition cannot tell whether the author considered its visibility and decided against it, or never +considered the question at all. ``@private`` makes that decision explicit. The alternatives are a +``# private`` comment, which no tool can reliably act on and which sits asymmetrically beside the +``@public`` decorators elsewhere in the file, or a leading underscore, which changes the name. + +**It preserves the option to promote a name later.** Marking ``function()`` private with a +decorator rather than by naming it ``_function()`` keeps the name stable. If it later joins the +public API, the change is to delete one decorator, rather than to rename the function and leave an +alias behind for the users who found the underscore version anyway. + +**It removes non-public names that come from elsewhere.** A module whose ``__all__`` comes from +somewhere else, whether written by hand or derived from the ``__all__`` of other modules, may +include names this module does not want to export. ``private()`` removes them at the point where +the reason for removing them is obvious. ``@public`` cannot express this, because there is nothing +to add. + +**Ordering follows ordinary Python semantics.** Declaring a name public and later private is not +indeterminate behavior. The calls run in the order they are written, like every other statement in +a module body, and the last one wins. Rejecting the combination would mean carrying a record of +every prior declaration in order to diagnose a construct whose meaning is already well defined. +Whether it is ever good style is a separate question, and one this PEP leaves to the author. + + .. _pep-844-static-analysis: Static analysis of the function call forms @@ -825,7 +865,7 @@ permits, and the obvious candidate, *list or tuple*, is unworkable: import time. See :ref:`pep-844-performance`. * ``private()`` could not be made symmetric. Python has no ``-=`` on tuples, so a tuple - ``__all__`` would accept declarations but refuse retractions. + ``__all__`` would accept declarations but refuse removals. Officially deprecating the non-list case was also considered and rejected. ``public()`` would accept a non-list ``__all__`` indefinitely, issue a :exc:`DeprecationWarning`, and decline to append From f07139924fab71fc87718ad774a381d814f5eeb5 Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Tue, 22 Sep 2026 10:39:26 -0700 Subject: [PATCH 6/7] PEP 844 updates --- peps/pep-0844.rst | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/peps/pep-0844.rst b/peps/pep-0844.rst index f45701f7d0d..d86e9ffabd3 100644 --- a/peps/pep-0844.rst +++ b/peps/pep-0844.rst @@ -341,10 +341,11 @@ 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 -(``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. +To address this, ``atpublic`` offers an optional install extra (``pip install atpublic[install]``) +that injects ``public`` and ``private`` into :mod:`builtins` at interpreter startup, so that no +import is needed. This extra pulls in a second distribution, ``atpublic-install``, which exists +only to provide a legacy ``.pth`` file and a :pep:`829` ``.start`` file that add the two names to +:mod:`builtins` as the interpreter comes up. Why decorators? @@ -543,7 +544,7 @@ fast implementation is simply *the* implementation, with no wheel platform suppo fallback path, and no optional extra. Moreover, a C implementation inside the interpreter can do less work than any third-party one: it has the calling frame in hand and can read its globals directly, where a pure Python implementation must call :func:`sys._getframe` on every call, as -``atpublic`` (as of version 8.0.0) does in all three forms. The name resolution that the single +``atpublic`` (as of version 8.0.1) does in all three forms. The name resolution that the single argument form performs is the same work either way. What the builtin saves is the frame lookup and the Python-level call itself. @@ -769,7 +770,7 @@ Reference Implementation The `atpublic `__ package, available on PyPI and maintained since 2016, implements the proposed semantics in pure Python. Its `source repository `__ is hosted on GitLab. The specification above describes -``atpublic`` 8.0.0, first released as 8.0.0a1 on 21-Aug-2026. +``atpublic`` 8.0.1, released on 21-Sep-2026. A CPython PR has not yet been written. @@ -889,8 +890,9 @@ Leave it on PyPI ---------------- 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). +functionality can simply add that library as a dependency and import the functions (or use the ``pip +install atpublic[install]`` extra, which pulls in the companion ``atpublic-install`` distribution to +populate builtins at startup). However, if this *is* a problem worth solving now, then leaving this in a third-party package on PyPI doesn't serve our users adequately. The need to include a dependency and an explicit import @@ -948,12 +950,15 @@ Footnotes Change History ============== -* 09-Sep-2026 +* 22-Sep-2026 * Update references to :pep:`842` (since withdrawn) and :pep:`843` (since published). - * Synchronize this PEP's proposed semantics with ``atpublic``'s 8.0.0 update. + * Synchronize this PEP's proposed semantics with ``atpublic`` 8.0.0, and its reference + implementation versions with the 8.0.1 and ``atpublic-install`` 1.0.0 releases of 21-Sep-2026. * Reject non-list ``__all__`` outright. The list requirement is kept, and only affects those modules that opt in to this PEP by calling ``public()`` or ``private()``. + * Say what the ``atpublic[install]`` extra costs outside the interpreter, now that it pulls in a + second distribution to populate :mod:`builtins` at startup. Copyright From e2f8be59495b06786ffcf32767503bce2ed61a52 Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Tue, 22 Sep 2026 10:59:19 -0700 Subject: [PATCH 7/7] Add a link to atpublic-install's PyPI stats --- peps/pep-0844.rst | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/peps/pep-0844.rst b/peps/pep-0844.rst index d86e9ffabd3..18bcbbb55df 100644 --- a/peps/pep-0844.rst +++ b/peps/pep-0844.rst @@ -345,7 +345,10 @@ To address this, ``atpublic`` offers an optional install extra (``pip install at that injects ``public`` and ``private`` into :mod:`builtins` at interpreter startup, so that no import is needed. This extra pulls in a second distribution, ``atpublic-install``, which exists only to provide a legacy ``.pth`` file and a :pep:`829` ``.start`` file that add the two names to -:mod:`builtins` as the interpreter comes up. +:mod:`builtins` as the interpreter comes up. Because that extra is now a separate distribution, +PyPI's `download counts `__ for +``atpublic-install``, relative to those for ``atpublic`` itself, give an imperfect +[#install-downloads]_ but ongoing measure of how much this convenience is useful. Why decorators? @@ -943,6 +946,9 @@ and to the contributors to and users of ``atpublic`` over the past decade. Footnotes ========= +.. [#install-downloads] The count is a lower bound. It misses anyone who calls ``install()`` + directly, and anyone who simply imports the two names, neither of which needs the extra at all. + .. [#import-magic] Except for a function call that *also* does the import, but that's even more magical.