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