diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c7277879ad1..7d5100fdbcc 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -716,6 +716,7 @@ peps/pep-0838.rst @AlexWaygood peps/pep-0840.rst @jeremyhylton @gvanrossum peps/pep-0841.rst @corona10 @sobolevn peps/pep-0842.rst @ZeroIntensity +peps/pep-0844.rst @warsaw # ... peps/pep-2026.rst @hugovk # ... diff --git a/peps/pep-0844.rst b/peps/pep-0844.rst new file mode 100644 index 00000000000..69a4561d121 --- /dev/null +++ b/peps/pep-0844.rst @@ -0,0 +1,719 @@ +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 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: + +.. 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 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 :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: + +* 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 :ref:`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 ``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 in which ``public()`` is called, 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, ``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. Appending is +idempotent, so a name that already appears in ``__all__`` is not added a second time. + + +``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(): + ... + +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 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. + + +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 +(``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? +--------------- + +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 function call form +----------------------------------------- + +The strongest objection to this proposal concerns the function 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 +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 +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 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 +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 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 +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: 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 +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 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 +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 +=================================== + +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 +--------------------- + +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__`` 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 +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: + +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 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 + 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 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 three times: + +.. 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, 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 +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 the 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. + +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. + + +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. + +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 repository +`__ is hosted on GitLab. + +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`. + +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 +============== + +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." + +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 + + public(SEVEN=7) + +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. + +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 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 +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 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 + 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 a 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.