diff --git a/pep_sphinx_extensions/__init__.py b/pep_sphinx_extensions/__init__.py index 109c09d7890..29570d0e1db 100644 --- a/pep_sphinx_extensions/__init__.py +++ b/pep_sphinx_extensions/__init__.py @@ -105,6 +105,8 @@ def setup(app: Sphinx) -> dict[str, bool]: app.add_directive("superseded", pep_banner_directive.SupersededBanner) app.add_directive("withdrawn", pep_banner_directive.WithdrawnBanner) + app.setup_extension("pep_sphinx_extensions.pep_processor.parsing.pep_soft_keyword") + # Register event callbacks app.connect("builder-inited", _update_config_for_builder) # Update configuration values for builder used app.connect("env-before-read-docs", create_pep_zero) # PEP 0 hook diff --git a/pep_sphinx_extensions/pep_processor/parsing/pep_soft_keyword.py b/pep_sphinx_extensions/pep_processor/parsing/pep_soft_keyword.py new file mode 100644 index 00000000000..8fec6664616 --- /dev/null +++ b/pep_sphinx_extensions/pep_processor/parsing/pep_soft_keyword.py @@ -0,0 +1,88 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from docutils import nodes +from pygments.lexers.python import PythonLexer +from pygments.token import Keyword, Name +from sphinx import addnodes + +if TYPE_CHECKING: + from collections.abc import Iterator + + from sphinx.application import Sphinx + from sphinx.environment import BuildEnvironment + +_LANG_PREFIX = "python+soft-keywords:" + + +def _keywords_from_language(language: str) -> tuple[str, ...] | None: + if not language.startswith(_LANG_PREFIX): + return None + keywords = tuple(w for w in language[len(_LANG_PREFIX) :].split(",") if w) + return keywords or None + + +def _soft_keyword_lexer(keywords: tuple[str, ...]) -> type[PythonLexer]: + words = frozenset(keywords) + + class SoftKeywordPythonLexer(PythonLexer): + name = f"Python (+ {', '.join(keywords)})" + aliases: list[str] = [] + filenames: list[str] = [] # don't shadow the real Python lexer + mimetypes: list[str] = [] + url = "" + + def get_tokens_unprocessed( + self, text: str, stack: tuple[str, ...] = ("root",) + ) -> Iterator[tuple[int, object, str]]: + for index, token, value in super().get_tokens_unprocessed(text, stack): + if value in words and token in Name: + yield index, Keyword, value + else: + yield index, token, value + + return SoftKeywordPythonLexer + + +def _init_env(app: Sphinx, env: BuildEnvironment, docnames: list[str]) -> None: + if not hasattr(env, "pep_soft_keywords"): + env.pep_soft_keywords = {} + + +def _collect_languages(app: Sphinx, doctree: nodes.document) -> None: + found = set() + for node in doctree.findall(nodes.literal_block): + if keywords := _keywords_from_language(node.get("language", "")): + found.add(keywords) + for node in doctree.findall(addnodes.highlightlang): + if keywords := _keywords_from_language(node.get("lang", "")): + found.add(keywords) + + if found: + app.env.pep_soft_keywords[app.env.docname] = found + else: + app.env.pep_soft_keywords.pop(app.env.docname, None) + + +def _merge_info( + app: Sphinx, + env: BuildEnvironment, + docnames: list[str], + other: BuildEnvironment, +) -> None: + env.pep_soft_keywords.update(getattr(other, "pep_soft_keywords", {})) + + +def _register_lexers(app: Sphinx, env: BuildEnvironment) -> None: + for keyword_sets in env.pep_soft_keywords.values(): + for keywords in keyword_sets: + app.add_lexer(_LANG_PREFIX + ",".join(keywords), _soft_keyword_lexer(keywords)) + + +def setup(app: Sphinx) -> dict[str, bool]: + app.connect("env-before-read-docs", _init_env) + app.connect("doctree-read", _collect_languages) + app.connect("env-merge-info", _merge_info) + app.connect("env-updated", _register_lexers) + return {"parallel_read_safe": True, "parallel_write_safe": True} diff --git a/peps/pep-0842.rst b/peps/pep-0842.rst index 4396efa8519..bb10f61f653 100644 --- a/peps/pep-0842.rst +++ b/peps/pep-0842.rst @@ -9,25 +9,26 @@ Python-Version: 3.16 Post-History: `24-Jul-2026 `__, `31-Jul-2026 `__ - Abstract ======== -This PEP proposes an ``__export__`` variable that modules can define to -express intent about the visibility of variables from outside the module. +This PEP proposes an ``export`` statement that modules can use to express +intent about the visibility of variables from outside the module. For example: -.. code-block:: python +.. code-block:: python+soft-keywords:export # spam.py - __export__ = ["Public"] + from mypackage export name - class Public: - ... + export foo = "42" + + export class Public: + pass class Private: - ... + pass .. code-block:: pycon @@ -40,12 +41,12 @@ For example: >>> spam.Public >>> spam.Private - :1: RuntimeWarning: 'Private' is not exported by 'spam' + :1: ExportWarning: 'Private' is not exported by 'spam' This is **not** intended to be an access modifier for Python; see -:ref:`pep-842-not-an-access-modifier`. +:ref:`the rationale `. Motivation @@ -145,7 +146,9 @@ The solution to this is to also prefix every imported name with ``_``: import tabnanny as _tabnanny -But, again, this sprinkles the code with even more underscored names. +But, again, this sprinkles the code with even more underscored names, and +doesn't necessarily send a crystal-clear message that the name is private; +see the next section. .. _pep-842-prefixed-public: @@ -159,7 +162,7 @@ users found useful functionality in a module's private API, and nothing was discouraging them from using it. In the standard library, a prime example of this is the :mod:`ctypes` module. -``ctypes`` is full of public APIs that are subject to Python's backwards +``ctypes`` is full of stable APIs that are subject to Python's backwards compatibility policy, but contain a leading underscore. For example: 1. :class:`ctypes._CFuncPtr` @@ -172,6 +175,140 @@ compatibility, or that an underscored name does not mean "private" in the module. In both cases, consumers are inclined to reach for more private names (because there's no apparent consequence for doing so), making this problem worse. +Modules aren't immune to this problem either. The standard :mod:`_thread` module, +for example, is prefixed with ``_`` while being public. + + +Some libraries have native counterparts +*************************************** + +In some cases, prefixing an import with ``_`` makes it ambiguous, because +some complicated modules come with :term:`extension modules ` +that provide access to native functionality or otherwise speed up the module +in some way. These native modules are often prefixed with a leading underscore. + +For example, in :term:`CPython`, the :mod:`asyncio` module has a private +``_asyncio`` accelerator module, so a reader seeing ``_asyncio`` may take it +to mean the C accelerator and not the normal module. + + +Imports are suggested by language servers and linters +----------------------------------------------------- + +Circling back to the issue described earlier, imports defined at the module-level +are visible as "public" names to the API surface. In fact, when developing a +module, the autocomplete provided by language servers will often suggest +importing modules that were also imported by that module. So, not only +are users not prevented from accessing seemingly-public imports, they may be +*encouraged* to do so by their language server! (This problem applies to any +name that is meant to be private; it's just that imports are a particularly +common case for this to occur.) + + +Real-world cases +**************** + +This is not a hypothetical problem. There are many real examples of this causing +issues in practice. + +.. note:: + + Special thanks to Hugo van Kemenade for `compiling this list + `__. + + +``os.errno`` +^^^^^^^^^^^^ + +In Python 3.7, an import to the :mod:`errno` module was removed from :mod:`os`. +This caused a lot of breakage: + +* `python/cpython#77847 `__ +* `Qiskit/qiskit#1253 `__ +* `uxlfoundation/oneMath#68 `__ +* `intel/bmap-tools#34 `__ +* `Red Hat Bug 1583196 `__ + + +``botocore.vendored`` +^^^^^^^^^^^^^^^^^^^^^ + +The `botocore `__ package had vendored +dependencies under the ``botocore.vendored`` namespace, which ended up +being `relied upon by users `__: + +* `boto/botocore#1466 `__ +* `AWS Developer Tools Blog `__ +* `aws/aws-cli#4082 `__ + + +``scipy`` and ``pandas`` +^^^^^^^^^^^^^^^^^^^^^^^^ + +Both the `scipy `__ and `pandas `__ +packages had other packages visible at the module-level, which had to be deprecated +and removed due to third-party usage: + +* `scipy/scipy#14889 `__ +* `scipy/scipy#19067 `__ +* `pandas-dev/pandas#30296 `__ +* `tdda/tdda#21 `__ + + +``scikit-learn`` +^^^^^^^^^^^^^^^^ + +The `scikit-learn `__ package vendored +``six`` and ``joblib``, which downstream packages then used and were broken +in v0.23: + +* `scikit-learn/scikit-learn#12916 `__ +* `scikit-learn-contrib/skope-rules#41 `__ +* `shubhomoydas/ad_examples#8 `__ +* `Kaggle Product Feedback `__ + + +Linters cannot fight against imports +************************************ + +As a solution to the above problem, one might suggest that linters should +simply warn against importing modules from another module. The primary issue +with this is that this pattern is particularly common in ``__init__.py`` files +to move all packages into one namespace. For example: + +.. code-block:: + + # __init__.py + + from my_package import subpackage_1 + from my_package import subpackage_2 + # etc + +Linters have no language-level way to distinguish this pattern from "standard" +imports. As a solution, many linters use ``import name as name`` to identify +intentional re-exports, but this pattern is only a convention. For example, +the above ``__init__.py`` would be rewritten as this: + +.. code-block:: + + # __init__.py + + from my_package import subpackage_1 as subpackage_1 + from my_package import subpackage_2 as subpackage_2 + # etc + + +Not only is this redundant (and a violation of the `DRY +principle `__), it's +confusing! Python's official documentation does not document this pattern for +re-exports (because it's not defined by the language and is only a convention +enforced by linters), so the packages that do this are primarily just "in +the know". + +But, because this is only a convention, linters can't enforce the negative case; +if an import is not given the ``name as name`` treatment, a linter can't necessarily +assume that an import is not a re-export. + We want to be nice to users, not shrug them away ------------------------------------------------ @@ -185,7 +322,7 @@ downstream breakage. In this case, the library maintainer has to make a decision to take place. 2. Commit to maintaining the private API as public, increasing the burden on themselves and encountering some of the problems described in - :ref:`pep-842-prefixed-public`. + :ref:`the motivation `. This PEP is not intended to solve this problem entirely, but instead is meant to mitigate it by making it much clearer that a user is accessing a private name; @@ -232,21 +369,47 @@ done through a module's ``__all__`` variable. This has two major downsides: be difficult to control namespace pollution and declare all public names in ``__all__`` simultaneously. -This PEP intends to solve both of these problems with a new ``__export__`` variable. +This PEP intends to solve both of these problems with a new ``__export__`` variable +and ``export`` statement. Specification ============= -.. _pep-842-export-requirements: +The ``ExportWarning`` type +-------------------------- + +A new warning category, called ``ExportWarning``, is added to the :mod:`builtins` +module. ``ExportWarning`` inherits from :class:`Warning` and defines no other +attributes. + +Though allowed, it is not intended to be emitted by user code; instead, it is +meant for emission by a :class:`module ` object when accessing +a name that is not in ``__export__``; see :ref:`pep-842-attribute-access`. -``__export__`` rules --------------------- +C API +***** + +.. note:: + + This section is specific to :term:`CPython`. -Object requirements -******************* +The ``ExportWarning`` class will be added to the public C API headers under +the name ``PyExc_ExportWarning``. As with all other global warning categories, +it will be in the :ref:`Stable ABI ` and will be :term:`immortal` +at runtime. + + +``__export__`` variables +------------------------ + + +.. _pep-842-export-requirements: + +Requirements +************ When defined in a module's global scope, ``__export__`` must be assigned to an object that implements :meth:`~object.__contains__` or :meth:`~object.__iter__` @@ -271,8 +434,15 @@ types are also valid assignments for ``__export__``: # '"name" in __export__' is valid, so this is okay +.. note:: + + When using one of the ``export`` syntax constructs as described later, + ``__export__`` must always be a ``list``, or otherwise be an object + with an ``append`` method that is always valid for ``str`` objects. + + Item requirements -***************** +^^^^^^^^^^^^^^^^^ It is not required that the strings inside ``__export__`` are actually names defined in the module (because it is not required for ``__export__`` to be a @@ -290,13 +460,15 @@ The caveat is that this will raise an exception when used with a wildcard import see :ref:`pep-842-implicit-all`. +.. _pep-842-attribute-access: + Module attribute access ------------------------ +*********************** When ``__export__`` is present in a module's globals, all access to attributes present on the module object will also check if the attribute name is present in ``__export__`` (via ``__contains__`` or through iteration, as specified previously). -If the attribute name is not present in ``__export__``, then a :exc:`RuntimeWarning` +If the attribute name is not present in ``__export__``, then an ``ExportWarning`` is emitted. For example: .. code-block:: python @@ -313,7 +485,7 @@ is emitted. For example: >>> spam.a 42 >>> spam.b - :1: RuntimeWarning: 'b' is not exported by 'spam' + :1: ExportWarning: 'b' is not exported by 'spam' 24 @@ -324,7 +496,7 @@ is emitted. For example: Dunder names -************ +^^^^^^^^^^^^ This does not apply to :term:`dunder` names; attributes such as :attr:`~object.__dict__` and :attr:`~module.__file__` will always be accessible on the module through @@ -344,7 +516,7 @@ For example: Module ``__getattr__`` functions --------------------------------- +******************************** The behavior of ``__export__`` cannot be overridden by a module's :meth:`~module.__getattr__` function, as ``__getattr__`` functions are only @@ -381,7 +553,7 @@ invoked for undefined names on modules. However, in cases where a module ``__dir__`` behavior --------------------- +******************** On a module with ``__export__``, the module's :meth:`~module.__dir__` function will be modified to exclude names that are not in the module's ``__export__``. @@ -408,7 +580,7 @@ in ``__export__``. For example: User-defined module ``__dir__`` functions -***************************************** +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ If a module defines its own ``__dir__`` method, it takes precedence over this behavior. It is up to the implementer of ``__dir__`` to exclude @@ -435,10 +607,10 @@ names that are not present in ``__export__``. For example: .. _pep-842-implicit-all: Implicit ``__all__`` definitions --------------------------------- +******************************** If a module defines ``__export__`` but does not define :attr:`~module.__all__`, -the ``__all__`` will be assigned to ``__export__``. To visualize: +then ``__all__`` will be assigned to ``__export__``. To visualize: .. code-block:: python @@ -487,7 +659,7 @@ be a valid ``__all__``. For example, including a name that does not exist in Semantic implementation ------------------------ +*********************** For a module, defining ``__export__`` is roughly equivalent to adding the following code: @@ -511,7 +683,7 @@ following code: return value if name not in __export__: - __import__("warnings").warn(f"{name!r} is not exported by {__name__!r}", RuntimeWarning, stacklevel=1) + __import__("warnings").warn(f"{name!r} is not exported by {__name__!r}", ExportWarning, stacklevel=1) return value @@ -527,16 +699,230 @@ following code: return names +Exporting names +--------------- + +Grammar +******* + +The grammar is changed to allow for the standalone ``export`` statement and +``export`` assignments: + +.. code-block:: peg + + export_stmt[stmt_ty]: + | "export" ','.NAME+ + | "export" assignment + + simple_stmt[stmt_ty] (memo): + | assignment + | &"export" export_stmt + + +Note that augmented assignments (``x += y``) are disallowed through a PEG action +at compile time. + + +Standalone exports +****************** + +A standalone ``export`` statement is a shorthand for appending one or more names +to a global ``__export__`` list. + +When the ``export`` statement is used, the interpreter first checks if each +name exists in the global scope. If any do not exist, a :class:`NameError` is +raised. The interpreter then checks if an ``__export__`` variable exists in +the global namespace. If not, it is assigned to an empty :class:`list` object. +Then, for each name used in the ``export`` statement, a :class:`str` containing +the name of the variable is passed as the first positional argument to the +:meth:`__export__.append ` method. + +To visualize, the following code: + +.. code-block:: python+soft-keywords:export + + export NAME1, NAME2 + +is semantically equivalent to: + +.. code-block:: python + + if "NAME1" not in globals(): + raise NameError(...) + + if "NAME2" not in globals(): + raise NameError(...) + + try: + __export__ + except NameError: + __export__ = [] + + __export__.append("NAME1") + __export__.append("NAME2") + + +The ``export`` statement is only allowed in the global namespace; using it +elsewhere (such as inside of a function body) raises a :class:`SyntaxError` +during compilation. + + +Export assignments +****************** + +When an assignment statement is prefixed with ``export``, the name is defined +and then ``export``\ ed. + +As an example, the following code: + +.. code-block:: python+soft-keywords:export + + export NAME1, NAME2 = VALUE1, VALUE2 + +is semantically equivalent to: + +.. code-block:: python+soft-keywords:export + + NAME1 = VALUE1 + NAME2 = VALUE2 + export NAME1, NAME2 + +"Export assignment" statements are valid when used with standard assignment +statements (``a = b``, ``a, b = c, d``, etc), and individual assignments +that contain a type annotation (``a: type = b``; in contrast, a standalone +``export a: type`` is not valid). For example, each of the following are valid: + +.. code-block:: python+soft-keywords:export + + export hello = "world" + export my, hovercraft = "full of", "eels" + export types_work_too: int = 42 + +The following are NOT valid: + +.. code-block:: python + + export hello: str + export my: str, hovercraft: str = "full of", "eels" + export name := "walrus" + export hello += "world" + + +Exporting functions and classes +------------------------------- + +Grammar +******* + +.. code-block:: peg + + export_compound_stmt[stmt_ty]: + | "export" (function_def | class_def) + + compound_stmt[stmt_ty]: + | &"export" export_compound_stmt + + +Behavior +******** + +A function definition or class definition statement can be prefixed with +``export`` to automatically export the name. + +To visualize, the following code: + +.. code-block:: python+soft-keywords:export + + export def NAME1(): + ... + + export class NAME2: + ... + +is semantically equivalent to: + +.. code-block:: python+soft-keywords:export + + def NAME1(): + ... + export NAME1 + + class NAME2: + ... + export NAME2 + + +As with assignments and standalone exports, using ``export def`` or +``export class`` outside of the global scope will raise a :class:`SyntaxError` +at compile time. + +There are no other caveats; all other syntax features of classes and functions +work when prefixed with ``export``. + + +The module re-export statement +------------------------------ + + +Grammar +******* + +A new rule is added and the existing ``import_from`` rule is modified: + +.. code-block:: peg + + import_or_export[expr_ty]: + | 'import' + | "export" + + import_from[stmt_ty]: + | "lazy"? 'from' ('.' | '...')* dotted_name import_or_export import_from_targets + | "lazy"? 'from' ('.' | '...')+ import_or_export import_from_targets + + +Behavior +******** + +The "module re-export statement" is an extension to the behavior of the +``from`` imports; it does the exact same thing, but also ``export``\ s each of +the imported names. + +For example, the following code: + +.. code-block:: python+soft-keywords:export + + from MODULE export NAME1, NAME2 + +is semantically equivalent to: + +.. code-block:: python+soft-keywords:export + + from MODULE import NAME1, NAME2 + export NAME1, NAME2 + +Similar to the other ``export`` constructs, this must occur at the module-level; +using it elsewhere is a :class:`SyntaxError`. + +Lazy imports, as described by :pep:`810`, are also allowed to be used with ``from`` +exports. For example: + +.. code-block:: python+soft-keywords:export + + lazy from foo export bar + +The existing rules for lazy imports apply here as well. + + Rationale ========= .. _pep-842-not-an-access-modifier: -``__export__`` is not an access modifier ----------------------------------------- +This is not an access modifier +------------------------------ This PEP does not aim to be a mechanism for preventing access to private -attributes in modules. The :exc:`RuntimeWarning` can be filtered away, +attributes in modules. The ``ExportWarning`` can be filtered away, disabled, or bypassed (such as by accessing attributes through the module's ``__dict__``). @@ -554,11 +940,42 @@ modules in the long term. Backwards Compatibility ======================= + +This does not require changes to existing code +---------------------------------------------- + +The functionality described in this PEP is only activated when a module defines +``__export__`` in the global scope (or by using the ``export`` statement, which +implicitly defines ``__export__``). Modules that do not do this will experience +the current behavior, where every name is exported by default. + + +``__export__`` overloads +------------------------ + This PEP has the potential to break users who were already defining global variables called ``__export__``. That said, the Python language reference :ref:`explicitly forbids ` users from doing this in the first place. +``export`` (soft) keyword +------------------------- + +``export``, as proposed by this PEP, is a :ref:`soft keyword `. +It does *not* break backwards compatibility, meaning that existing code using +"``export``" as a variable name will continue to work. + + +Relation to ``-W error`` +------------------------ + +While this PEP does not break any existing applications, it may break +tests for downstream users of packages who choose to adopt this PEP, as +many popular testing frameworks, such as `pytest +`__, run with warnings-as-errors enabled +by default. + + Security Implications ===================== @@ -568,7 +985,12 @@ This PEP has no known security implications. How to Teach This ================= -``__export__`` will be documented as part of the language standard. +Both the ``export`` statement and the ``__export__`` variable will +be documented as part of the language standard. + + +Maintaining backwards compatible codebases +------------------------------------------ To help adoption, it will be recommended that users define both ``__all__`` and ``__export__`` in their modules. This allows code on Python 3.16+ to get @@ -581,11 +1003,18 @@ attribute. In practice, this should look something like this: __export__ = __all__ + ["eels"] +Or, if the package's ``__all__`` is equivalent to ``__export__``: + +.. code-block:: python + + __export__ = __all__ + + Reference Implementation ======================== A reference implementation of this PEP can be found -`here `__. +`here `__. Performance ----------- @@ -616,34 +1045,14 @@ a wildcard import, so the developer chooses to not include them in ``__all__``, but users of static typing will still want access to these type aliases for annotating their own code. - -Add new ``export`` syntax -------------------------- - -Rather than simply defining exported names in a global variable, it was -`suggested `__ to take this proposal a -step further and add a true ``export`` (soft) keyword to Python that would -effectively auto-generate an ``__export__`` variable at runtime, like so: - -.. code-block:: python - - export class Foo: - ... - - export MY_CONST = 42 - - # __export__ would now be set to ['Foo', 'MY_CONST'] - - -This is feasible, and may very well become part of Python someday, but the timing -did not feel right. Adding new syntax to Python requires a lot of demand and -community feedback, and it was unclear whether there was enough demand for this -feature to warrant new syntax. - -That said, it is expected that if this PEP is accepted, libraries will take -advantage of ``__export__`` to build APIs that replicate the proposed ``export`` -syntax. Third-party solutions and widespread adoption would make it much clearer -that new syntax is the best choice for Python in the long run. +In addition, it's not clear that there's any good spelling for this +behavior that covers all cases. The "obvious" solution is to add a +new :ref:`future statement ` that makes ``__all__`` more strict, but +that isn't backwards compatible; codebases wanting to opt-in to the behavior +described by this PEP must use a spelling that works on all supported Python +versions in order to keep their code working on older versions, so any +solutions that add special functionality to ``__all__`` generally will not +work. Raising an exception upon accessing unexported attributes @@ -669,6 +1078,80 @@ As such, this proposal switched to emitting warnings when accessing unexported names. +Introduce ``__export__`` on its own +----------------------------------- + +The original revision of this proposal included ``__export__`` as a standalone +variable and did not provide any new syntax. The appeal of this was that it +was backwards compatible; projects could simply write ``__export__ = __all__``, +and then when users upgraded to a version that supported ``__export__``, they +would get the documentation and enforcement benefits described by this PEP. + +It was eventually decided that this was too conservative, because while +``__export__`` was compatible with ``__all__``, it shared many of the same +problems with it, such as forgetting to add or remove items from the list. + +To `quote `__ Guido van Rossum: + + 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. + + +Add a ``private`` keyword for class bodies +------------------------------------------ + +During discussion of this proposal, it was suggested to add a ``private`` +keyword for use in classes. For example: + +.. code-block:: python+soft-keywords:private + + class Something: + private def hello(self): + print("Hello, world!") + + +This was rejected primarily because it does not have a clear benefit over +the existing :ref:`name mangling behavior ` (using +the ``__`` prefix), which also solves many of the problems described in the +motivation of this PEP. + +Additionally, this is much more difficult to implement. The author's reference +implementation involved new access protocols, disabling optimizations, and overall +much more complexity when compared to the simple modification to the default +``module.__getattr__`` behavior required by ``__export__``. + + +Add ``public`` and ``private`` decorators as builtins +----------------------------------------------------- + +Instead of adding a new ``export`` keyword, it was suggested to add ``private`` +and ``public`` decorators, based on Barry Warsaw's `atpublic `__ +package, to the :mod:`builtins` module. + +The decorators would have provided the same documentation aspect of this +PEP, and potentially the same enforcement aspect, without the need for new +syntax. For example: + +.. code-block:: python + + @public + class MyPublicClass: + ... + + # Or + @private + class MyPrivateClass: + ... + + +This is the author's next preferred solution after ``export`` syntax, but it +does come with some caveats. In particular, there's no easy way to export +simple variables without duplicating the name, which many dislike due to the +violation of the DRY principle. + + Open Issues =========== @@ -682,10 +1165,20 @@ Thanks to Hugo van Kemenade and Savannah Ostrowski for `inspiring `__ the idea behind this PEP. +In addition, the design behind this PEP was largely influenced by discussion +and ideas from many people, including, but not limited to, Guido van Rossum, +Paul Moore, Steve Dower, and Barry Warsaw. + Change History ============== +* 05-Aug-2026 + + - Added an ``export`` statement. + - Added the ``ExportWarning`` builtin type, which is now emitted instead of a + :exc:`RuntimeWarning` when accessing unexported attributes. + * 01-Aug-2026 - Accessing an unexported attribute now emits a :exc:`RuntimeWarning` instead