@@ -49,7 +49,8 @@ For example:
4949
5050
5151 This is **not ** intended to be an access modifier for Python; see
52- :ref: `the rationale <pep-842-not-an-access-modifier >`.
52+ :ref: `the rationale <pep-842-not-an-access-modifier >`. The mechanisms specified
53+ by this PEP are easy to work around if necessary.
5354
5455
5556Motivation
@@ -207,7 +208,8 @@ importing modules that were also imported by that module. So, not only
207208are users not prevented from accessing seemingly-public imports, they may be
208209*encouraged * to do so by their language server! (This problem applies to any
209210name that is meant to be private; it's just that imports are a particularly
210- common case for this to occur.)
211+ common case for this to occur. For other examples, see :ref: `below
212+ <pep-842-accidental-private-access>`)
211213
212214
213215Real-world cases
@@ -235,10 +237,26 @@ This caused a lot of breakage:
235237* `Red Hat Bug 1583196 <https://bugzilla.redhat.com/show_bug.cgi?id=1583196 >`__
236238
237239
240+ ``requests.packages ``
241+ ^^^^^^^^^^^^^^^^^^^^^
242+
243+ The `requests <https://requests.readthedocs.io/en/latest/ >`__ package
244+ had an internal vendoring namespace that users treated as an API, so
245+ after unvendoring packages, ``requests `` kept ``requests.packages ``
246+ as an alias, which led to its own subtle breakage:
247+
248+ * `psf/requests#3985 <https://github.com/psf/requests/issues/3985 >`__
249+ * `psf/requests#4102 <https://github.com/psf/requests/issues/4102 >`__
250+ * `psf/requests#4104 <https://github.com/psf/requests/issues/4104 >`__
251+ * `psf/requests#5327 <https://github.com/psf/requests/issues/5327 >`__
252+ * `psf/requests#5561 <https://github.com/psf/requests/issues/5561 >`__
253+ * `urllib3/urllib3#1518 <https://github.com/urllib3/urllib3/issues/1518 >`__
254+
255+
238256``botocore.vendored ``
239257^^^^^^^^^^^^^^^^^^^^^
240258
241- The `botocore <https://github.com/boto/botocore >`__ package had vendored
259+ The `botocore <https://github.com/boto/botocore >`__ package also had vendored
242260dependencies under the ``botocore.vendored `` namespace, which ended up
243261being `relied upon by users <https://github.com/search?q=%22botocore.vendored.requests.packages%22&type=code >`__:
244262
@@ -273,6 +291,84 @@ and broke when they were removed in v0.23:
273291* `Kaggle Product Feedback <https://www.kaggle.com/discussions/product-feedback/158412 >`__
274292
275293
294+ .. _pep-842-accidental-private-access :
295+
296+ Other examples
297+ **************
298+
299+ Beyond imports, there are several examples where users accidentally accessed
300+ internal APIs, which resulted in breakage.
301+
302+
303+ ``logging._acquireLock `` / ``logging._releaseLock ``
304+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
305+
306+ The documentation for the :mod: `logging ` module included a private API
307+ in an example. This example was then copy-pasted to several downstream projects,
308+ and was broken when the private APIs were removed in Python 3.13:
309+
310+ - `sqlmapproject/sqlmap#5731 <https://github.com/sqlmapproject/sqlmap/issues/5731 >`__
311+ - `sqlmapproject/sqlmap#5796 <https://github.com/sqlmapproject/sqlmap/issues/5796 >`__
312+ - `conda/conda#14439 <https://github.com/conda/conda/issues/14439 >`__
313+ - `madphysicist/haggis#2 <https://gitlab.com/madphysicist/haggis/-/work_items/2 >`__
314+ - `Debian Bug#1088763 <https://www.mail-archive.com/debian-bugs-dist@lists.debian.org/msg2003691.html >`__
315+
316+
317+ ``matplotlib.cbook._check_in_list `` / ``matplotlib.cbook._rename_parameter ``
318+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
319+
320+ `matplotlib <https://matplotlib.org/ >`__ left some utility functions in a
321+ module-level namespace. These functions were prefixed with a leading underscore,
322+ but users disregarded this, leading to breakage when they were removed:
323+
324+ - `matplotlib/matplotlib#18494 <https://github.com/matplotlib/matplotlib/pull/18494 >`__
325+ - `dougcahl/eddy_identification_winding#1 <https://github.com/dougcahl/eddy_identification_winding/issues/1 >`__
326+ - `guchengxi1994/mask2json#58 <https://github.com/guchengxi1994/mask2json/issues/58 >`__
327+
328+
329+ ``concurrent.futures.thread._threads_queues ``
330+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
331+
332+ In Python 3.8, a recipe to make :class: `~concurrent.futures.ThreadPoolExecutor `
333+ be killed by CTRL+C was spread around. This recipe used the internal API, and
334+ was missed by many users (or potentially seen, but ignored, due to the issues
335+ described :ref: `above <pep-842-prefixed-public >`), leading to breakage in Python
336+ 3.9 when worker threads stopped being daemon:
337+
338+ - `clchiou/non_graceful_shutdown.py <https://gist.github.com/clchiou/f2608cbe54403edb0b13 >`__
339+ - `python/cpython#83993 <https://github.com/python/cpython/issues/83993 >`__
340+ - `cognitedata/cognite-sdk-python#1122 <https://github.com/cognitedata/cognite-sdk-python/pull/1122 >`__
341+ - `Opentrons/opentrons#12970 <https://github.com/Opentrons/opentrons/pull/12970 >`__
342+
343+
344+ ``re._pattern_type ``
345+ ^^^^^^^^^^^^^^^^^^^^
346+
347+ Before the existence of :class: `re.Pattern `, the type of objects returned by
348+ :func: `re.compile ` was private. Many users found it easier to access the internal
349+ type rather than do ``type(re.compile('')) ``, which led to breakage in 3.7 when
350+ it was removed:
351+
352+ - `beetbox/beets#2986 <https://github.com/beetbox/beets/issues/2986 >`__
353+ - `django-precise-bbcode#25 <https://github.com/ellmetha/django-precise-bbcode/issues/25 >`__
354+ - `python/cpython#1646 <https://github.com/python/cpython/pull/1646 >`__
355+
356+
357+ ``asyncio.staggered_race ``
358+ ^^^^^^^^^^^^^^^^^^^^^^^^^^
359+
360+ The `aiohappyeyeballs <https://aiohappyeyeballs.aio-libs.org/ >`__ package
361+ (which is internally used by `aiohttp <https://docs.aiohttp.org/ >`__) used
362+ the internal ``staggered_race `` API from the :mod: `asyncio ` module.
363+ This broke when the implementation was updated to no longer have a ``loop ``
364+ parameter:
365+
366+ - `aio-libs/aiohttp#8599 <https://github.com/aio-libs/aiohttp/issues/8599 >`__
367+ - `python/cpython#124639 <https://github.com/python/cpython/issues/124639 >`__
368+ - `python/cpython#124390 <https://github.com/python/cpython/pull/124390 >`__
369+ - `python/cpython#124700 <https://github.com/python/cpython/pull/124700 >`__
370+
371+
276372Linters cannot fight against imports
277373************************************
278374
@@ -701,8 +797,8 @@ The grammar is changed to allow for the standalone ``export`` statement and
701797 | &"export" export_stmt
702798
703799
704- Note that augmented assignments (``x += y ``) are disallowed through a PEG action
705- at compile time.
800+ Note that augmented assignments (``x += y ``), subscripts (`` x[y] = z ``), and
801+ attributes (`` x.y = z ``) are disallowed through a PEG action at compile time.
706802
707803
708804Standalone exports
@@ -788,6 +884,8 @@ The following are NOT valid:
788884 export my: str , hovercraft: str = " full of" , " eels"
789885 export name := " walrus"
790886 export hello += " world"
887+ export trees[0 ] = " the larch"
888+ export something.name = " python"
791889
792890
793891 Exporting functions and classes
@@ -811,7 +909,7 @@ following is not valid:
811909
812910.. code-block :: python
813911
814- def export name(): # NOT VALID
912+ async def export name(): # NOT VALID
815913 ...
816914
817915 class export Name: # NOT VALID
@@ -917,8 +1015,8 @@ This is not an access modifier
9171015------------------------------
9181016
9191017This PEP does not aim to be a mechanism for preventing access to private
920- attributes in modules. The ``ExportError `` can be bypassed (such as by
921- accessing attributes through the module's `` __dict__ ``) .
1018+ attributes in modules. The ``ExportError `` can be bypassed and avoided;
1019+ see :ref: ` below < pep-842-bypassing-export >` .
9221020
9231021This is by design. Python does not include access modifiers as a language
9241022feature for a reason. To `quote <https://discuss.python.org/t/104994/2 >`__ Eric Smith:
@@ -1006,6 +1104,43 @@ in Python, names are public (as in, importable) by default, *except* when
10061104for ``export `` in Python is "make everything else private except for this name".
10071105
10081106
1107+ .. _pep-842-bypassing-export :
1108+
1109+ Bypassing ``__export__ ``
1110+ ------------------------
1111+
1112+ As mentioned previously, this proposal is not meant to be an ironclad
1113+ shield around private variables.
1114+
1115+ For prototyping, the simplest way to get around ``__export__ `` is to simply
1116+ delete it:
1117+
1118+ .. code-block :: python
1119+
1120+ import module
1121+
1122+ del module.__export__
1123+ # All private variables in 'module' are now available
1124+
1125+ Or, for a more granular workaround, append specific private names to ``__export__ ``:
1126+
1127+ .. code-block :: python
1128+
1129+ import module
1130+
1131+ module.__export__.append(" name_you_want" )
1132+
1133+ However, this approach modifies the ``__export__ `` list globally, meaning that
1134+ enforcement inside other packages will also be disabled. To avoid this, access
1135+ private variables through the module's ``__dict__ ``:
1136+
1137+ .. code-block :: python
1138+
1139+ import module
1140+
1141+ name_you_want = module.__dict__ [" name_you_want" ]
1142+
1143+
10091144 Reference Implementation
10101145========================
10111146
@@ -1058,9 +1193,11 @@ Emitting a warning upon accessing unexported attributes
10581193This PEP initially proposed raising an :exc: `ImportError ` upon accessing
10591194module attributes that were not listed in ``__export__ ``. This was not
10601195well received, as the PEP did not clearly describe the intentions behind
1061- the proposal, and as such, many rejected the notion of "private attributes"
1062- as a knee-jerk reaction. Following that feedback, the ``ImportError `` turned
1063- into a warning, which was eventually determined to be a bad compromise.
1196+ the proposal. Following that feedback, the ``ImportError `` turned
1197+ into a warning, which was eventually determined to be a bad compromise,
1198+ as the ergonomics of warnings are much worse than exceptions, and because
1199+ many testing frameworks (such as ``pytest ``) turn warnings into exceptions
1200+ during testing.
10641201
10651202
10661203Introduce ``__export__ `` on its own
@@ -1100,16 +1237,8 @@ For example:
11001237 export def goodbye (self ):
11011238 print (" Goodbye, world!" )
11021239
1103-
1104- This was rejected primarily because it does not have a clear benefit over
1105- the existing :ref: `name mangling behavior <private-name-mangling >` (using
1106- the ``__ `` prefix), which also solves many of the problems described in the
1107- motivation of this PEP.
1108-
1109- Additionally, this is much more difficult to implement. The author's reference
1110- implementation involved new access protocols, disabling optimizations, and overall
1111- much more complexity when compared to the simple modification to the default
1112- ``module.__getattribute__ `` behavior required by ``__export__ ``.
1240+ This is considered out of scope for this PEP, and may be revisited by a
1241+ future proposal.
11131242
11141243
11151244Add ``public `` and ``private `` decorators as builtins
@@ -1192,20 +1321,39 @@ How should packages have access to their own private members?
11921321
11931322Imagine that a package has two modules:
11941323
1195- 1. ``library/_utils .py ``, which is meant to contain utilities that are only
1324+ 1. ``library/utils .py ``, which is meant to contain utilities that are only
11961325 for the developer of ``library ``.
119713262. ``library/main.py ``, which holds public APIs that are usable to the
11981327 users of ``library ``.
11991328
1200- The names in ``_utils .py `` are not exported, because the module is not intended
1329+ The names in ``utils .py `` are not exported, because the module is not intended
12011330to be accessed by users of ``library ``. But, ``main.py `` should have access to
12021331these names; the current proposal would result in ``main.py `` getting an
1203- ``ExportError `` upon importing private names from ``_utils .py ``.
1332+ ``ExportError `` upon importing private names from ``utils .py ``.
12041333
1205- How should this be resolved? Is this necessary at all -- as in, should ``_utils .py ``
1334+ How should this be resolved? Is this necessary at all -- as in, should ``utils .py ``
12061335mark its utilities as exported, and ask that users don't import anything from it?
12071336
12081337
1338+ Does ``export `` need a top-level marker?
1339+ ----------------------------------------
1340+
1341+ Usage of ``export `` affects the runtime behavior of all other names defined
1342+ in a module, so it has been argued that this can make maintenance more difficult
1343+ in some cases. For example, if a developer is unsure whether a module already
1344+ uses ``export ``, they would have to search the module for it in order to know
1345+ whether it is safe to declare a public API as ``export `` without affecting
1346+ the rest of the code.
1347+
1348+ As a solution, it was proposed to require ``export `` syntax to have some sort
1349+ of marker at the top of the module (such as an ``__export__ = [] `` declaration
1350+ or a ``__future__ `` import). This has not been decided upon yet, because it is
1351+ unclear whether the problem described above will actually turn out to be an
1352+ issue in practice; it is expected that many libraries will be consistent about
1353+ their usage of ``export ``/``__export__ `` internally, and thus it should not be
1354+ very difficult for a developer to know what kind of module they are working in.
1355+
1356+
12091357Acknowledgements
12101358================
12111359
@@ -1221,6 +1369,15 @@ Paul Moore, Steve Dower, and Barry Warsaw.
12211369Change History
12221370==============
12231371
1372+ * 12-Aug-2026
1373+
1374+ - Clarified whether the ``export `` statement works with subscripts and
1375+ attribute assignments.
1376+ - Added an open issue on whether ``export `` syntax should be necessary
1377+ at the top of the file.
1378+ - Added more examples for real-world cases.
1379+ - Added a section in "How To Teach This" about how to bypass ``__export__ ``.
1380+
12241381* 11-Aug-2026
12251382
12261383 - Required ``__export__ `` to always be a :class: `list ` object.
0 commit comments