Skip to content

Commit 3b8bf7c

Browse files
authored
PEP 810: Update decisions on with blocks and __dict__ reification (#4656)
1 parent 8fc0771 commit 3b8bf7c

1 file changed

Lines changed: 91 additions & 110 deletions

File tree

peps/pep-0810.rst

Lines changed: 91 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ Syntax restrictions
246246
~~~~~~~~~~~~~~~~~~~
247247

248248
The soft keyword is only allowed at the global (module) level, **not** inside
249-
functions, class bodies, with ``try``/``with`` blocks, or ``import *``. Import
249+
functions, class bodies, ``try`` blocks, or ``import *``. Import
250250
statements that use the soft keyword are *potentially lazy*. Imports that
251251
can't be lazy are unaffected by the global lazy imports flag, and instead are
252252
always eager. Additionally, ``from __future__ import`` statements cannot be
@@ -270,10 +270,6 @@ Examples of syntax errors:
270270
except ImportError:
271271
pass
272272
273-
# SyntaxError: lazy import not allowed inside with blocks
274-
with suppress(ImportError):
275-
lazy import json
276-
277273
# SyntaxError: lazy from ... import * is not allowed
278274
lazy from json import *
279275
@@ -465,54 +461,37 @@ immediately resolve all lazy objects (e.g. ``lazy from`` statements) that
465461
referenced the module. It **only** resolves the lazy object being accessed.
466462

467463
Accessing a lazy object (from a global variable or a module attribute) reifies
468-
the object. Accessing a module's ``__dict__`` reifies **all** lazy objects in
469-
that module. Calling ``dir()`` at the global scope will not reify the globals
470-
and calling ``dir(mod)`` will be special cased in ``mod.__dir__`` avoid
471-
reification as well.
472-
473-
Example using ``__dict__`` from external code:
474-
475-
.. code-block:: python
476-
477-
# my_module.py
478-
import sys
479-
lazy import json
464+
the object.
480465

481-
print('json' in sys.modules) # False - still lazy
482-
483-
# main.py
484-
import sys
485-
import my_module
486-
487-
# Accessing __dict__ from external code DOES reify all lazy imports
488-
d = my_module.__dict__
489-
490-
print('json' in sys.modules) # True - reified by __dict__ access
491-
print(type(d['json'])) # <class 'module'>
466+
However, calling ``globals()`` or accessing a module's ``__dict__`` does
467+
**not** trigger reification -- they return the module's dictionary, and
468+
accessing lazy objects through that dictionary still returns lazy proxy
469+
objects that need to be manually reified upon use. A lazy object can be
470+
resolved explicitly by calling the ``resolve`` method. Calling ``dir()`` at
471+
the global scope will not reify the globals, nor will calling ``dir(mod)``
472+
(through special-casing in ``mod.__dir__``.) Other, more indirect ways of
473+
accessing arbitrary globals (e.g. inspecting ``frame.f_globals``) also do
474+
**not** reify all the objects.
492475

493-
However, calling ``globals()`` does **not** trigger reification -- it returns
494-
the module's dictionary, and accessing lazy objects through that dictionary
495-
still returns lazy proxy objects that need to be manually reified upon use. A
496-
lazy object can be resolved explicitly by calling the ``resolve`` method.
497-
Other, more indirect ways of accessing arbitrary globals (e.g. inspecting
498-
``frame.f_globals``) also do **not** reify all the objects.
499-
500-
Example using ``globals()``:
476+
Example using ``globals()`` and ``__dict__``:
501477

502478
.. code-block:: python
503479
480+
# my_module.py
504481
import sys
505482
lazy import json
506483
507484
# Calling globals() does NOT trigger reification
508485
g = globals()
509-
510486
print('json' in sys.modules) # False - still lazy
511487
print(type(g['json'])) # <class 'LazyImport'>
512488
489+
# Accessing __dict__ also does NOT trigger reification
490+
d = __dict__
491+
print(type(d['json'])) # <class 'LazyImport'>
492+
513493
# Explicitly reify using the resolve() method
514494
resolved = g['json'].resolve()
515-
516495
print(type(resolved)) # <class 'module'>
517496
print('json' in sys.modules) # True - now loaded
518497
@@ -707,15 +686,15 @@ Where ``<mode>`` can be:
707686

708687
* ``"normal"`` (or unset): Only explicitly marked lazy imports are lazy
709688

710-
* ``"all"``: All module-level imports (except in ``try`` or ``with``
689+
* ``"all"``: All module-level imports (except in ``try``
711690
blocks and ``import *``) become *potentially lazy*
712691

713692
* ``"none"``: No imports are lazy, even those explicitly marked with
714693
``lazy`` keyword
715694

716695
When the global flag is set to ``"all"``, all imports at the global level
717-
of all modules are *potentially lazy* **except** for those inside a ``try`` or
718-
``with`` block or any wild card (``from ... import *``) import.
696+
of all modules are *potentially lazy* **except** for those inside a ``try``
697+
block or any wild card (``from ... import *``) import.
719698

720699
If the global lazy imports flag is set to ``"none"``, no *potentially
721700
lazy* import is ever imported lazily, the import filter is never called, and
@@ -1251,36 +1230,9 @@ either the lazy proxy or the final resolved object.
12511230
Can I force reification of a lazy import without using it?
12521231
----------------------------------------------------------
12531232

1254-
Yes, accessing a module's ``__dict__`` will reify all lazy objects in that
1255-
module. Individual lazy objects can be resolved by calling their ``resolve()``
1233+
Yes, individual lazy objects can be resolved by calling their ``resolve()``
12561234
method.
12571235

1258-
What's the difference between ``globals()`` and ``mod.__dict__`` for lazy imports?
1259-
----------------------------------------------------------------------------------
1260-
1261-
Calling ``globals()`` returns the module's dictionary without reifying lazy
1262-
imports -- you'll see lazy proxy objects when accessing them through the
1263-
returned dictionary. However, accessing ``mod.__dict__`` from external code
1264-
reifies all lazy imports in that module first. This design ensures:
1265-
1266-
.. code-block:: python
1267-
1268-
# In your module:
1269-
lazy import json
1270-
1271-
g = globals()
1272-
print(type(g['json'])) # <class 'LazyImport'> - your problem
1273-
1274-
# From external code:
1275-
import sys
1276-
mod = sys.modules['your_module']
1277-
d = mod.__dict__
1278-
print(type(d['json'])) # <class 'module'> - reified for external access
1279-
1280-
This distinction means adding lazy imports and calling ``globals()`` is your
1281-
responsibility to manage, while external code accessing ``mod.__dict__``
1282-
always sees fully loaded modules.
1283-
12841236
Why not use ``importlib.util.LazyLoader`` instead?
12851237
--------------------------------------------------
12861238

@@ -1664,6 +1616,52 @@ From the discussion on :pep:`690` it is clear that this is a fairly
16641616
contentious idea, although perhaps once we have wide-spread use of lazy
16651617
imports this can be reconsidered.
16661618

1619+
Disallowing lazy imports inside ``with`` blocks
1620+
------------------------------------------------
1621+
1622+
An earlier version of this PEP proposed disallowing ``lazy import`` statements
1623+
inside ``with`` blocks, similar to the restriction on ``try`` blocks. The
1624+
concern was that certain context managers (like ``contextlib.suppress(ImportError)``)
1625+
could suppress import errors in confusing ways when combined with lazy imports.
1626+
1627+
However, this restriction was rejected because ``with`` statements have much
1628+
broader semantics than ``try/except`` blocks. While ``try/except`` is explicitly
1629+
about catching exceptions, ``with`` blocks are commonly used for resource
1630+
management, temporary state changes, or scoping -- contexts where lazy imports
1631+
work perfectly fine. The ``lazy import`` syntax is explicit enough that
1632+
developers who write it inside a ``with`` block are making an intentional choice,
1633+
aligning with Python's "consenting adults" philosophy. For genuinely problematic
1634+
cases like ``with suppress(ImportError): lazy import foo``, static analysis
1635+
tools and linters are better suited to catch these patterns than hard language
1636+
restrictions.
1637+
1638+
Forcing eager imports in ``with`` blocks under the global flag
1639+
---------------------------------------------------------------
1640+
1641+
Another rejected idea was to make imports inside ``with`` blocks remain eager
1642+
even when the global lazy imports flag is set to ``"all"``. The rationale was
1643+
to be conservative: since ``with`` statements can affect how imports behave
1644+
(e.g., by modifying ``sys.path`` or suppressing exceptions), forcing imports to
1645+
remain eager could prevent subtle bugs. However, this would create inconsistent
1646+
behavior where ``lazy import`` is allowed explicitly in ``with`` blocks, but
1647+
normal imports remain eager when the global flag is enabled. This inconsistency
1648+
between explicit and implicit laziness is confusing and hard to explain.
1649+
1650+
The simpler, more consistent rule is that the global flag affects imports
1651+
everywhere that explicit ``lazy import`` syntax is allowed. This avoids having
1652+
three different sets of rules (explicit syntax, global flag behavior, and filter
1653+
mechanism) and instead provides two: explicit syntax rules match what the global
1654+
flag affects, and the filter mechanism provides escape hatches for edge cases.
1655+
For users who need fine-grained control, the filter mechanism
1656+
(``sys.set_lazy_imports_filter()``) already provides a way to exclude specific
1657+
imports or patterns. Additionally, there's no inverse operation: if the global
1658+
flag forces imports eager in ``with`` blocks but a user wants them lazy, there's
1659+
no way to override it, creating an asymmetry.
1660+
1661+
In summary: imports in ``with`` blocks behave consistently whether marked
1662+
explicitly with ``lazy import`` or implicitly via the global flag, creating a
1663+
simple rule that's easy to explain and reason about.
1664+
16671665
Modification of the dict object
16681666
-------------------------------
16691667

@@ -1868,55 +1866,38 @@ from a real dict in almost all cases, which is extremely difficult to achieve
18681866
correctly. Any deviation from true dict behavior would be a source of subtle
18691867
bugs.
18701868

1871-
Reifying lazy imports when ``globals()`` is called
1872-
---------------------------------------------------
1869+
Automatically reifying on ``__dict__`` or ``globals()`` access
1870+
--------------------------------------------------------------
18731871

1874-
Calling ``globals()`` returns the module's namespace dictionary without
1875-
triggering reification of lazy imports. Accessing lazy objects through the
1876-
returned dictionary yields the lazy proxy objects themselves. This is an
1877-
intentional design decision for several reasons:
1878-
1879-
**The key distinction**: Adding a lazy import and calling ``globals()`` is the
1880-
module author's concern and under their control. However, accessing
1881-
``mod.__dict__`` from external code is a different scenario -- it crosses
1882-
module boundaries and affects someone else's code. Therefore, ``mod.__dict__``
1883-
access reifies all lazy imports to ensure external code sees fully realized
1884-
modules, while ``globals()`` preserves lazy objects for the module's own
1885-
introspection needs.
1886-
1887-
**Technical challenges**: It is impossible to safely reify on-demand when
1888-
``globals()`` is called because we cannot return a proxy dictionary -- this
1889-
would break common usages like passing the result to ``exec()`` or other
1890-
built-ins that expect a real dictionary. The only alternative would be to
1891-
eagerly reify all lazy imports whenever ``globals()`` is called, but this
1892-
behavior would be surprising and potentially expensive.
1893-
1894-
**Performance concerns**: It is impractical to cache whether a reification
1895-
scan has been performed with just the globals dictionary reference, whereas
1896-
module attribute access (the primary use case) can efficiently cache
1897-
reification state in the module object itself.
1898-
1899-
**Use case rationale**: The chosen design makes sense precisely because of
1900-
this distinction: adding a lazy import and calling ``globals()`` is your
1901-
problem to manage, while having lazy imports visible in ``mod.__dict__``
1902-
becomes someone else's problem. By reifying on ``__dict__`` access but not on
1903-
``globals()``, we ensure external code always sees fully loaded modules while
1904-
giving module authors control over their own introspection.
1905-
1906-
Note that three options were considered:
1872+
Three options were considered for how ``globals()`` and ``mod.__dict__`` should
1873+
behave with lazy imports:
19071874

19081875
1. Calling ``globals()`` or ``mod.__dict__`` traverses and resolves all lazy
19091876
objects before returning.
19101877
2. Calling ``globals()`` or ``mod.__dict__`` returns the dictionary with lazy
1911-
objects present.
1878+
objects present (chosen).
19121879
3. Calling ``globals()`` returns the dictionary with lazy objects, but
19131880
``mod.__dict__`` reifies everything.
19141881

1915-
We chose the third option because it properly delineates responsibility: if
1916-
you add lazy imports to your module and call ``globals()``, you're responsible
1917-
for handling the lazy objects. But external code accessing your module's
1918-
``__dict__`` shouldn't need to know about your lazy imports -- it gets fully
1919-
resolved modules.
1882+
We chose option 2: both ``globals()`` and ``__dict__`` return the raw
1883+
namespace dictionary without triggering reification. This provides a clean,
1884+
predictable model where low-level introspection APIs don't trigger side
1885+
effects.
1886+
1887+
Having ``globals()`` and ``__dict__`` behave identically creates symmetry and
1888+
a simple mental model: both expose the raw namespace view. Low-level
1889+
introspection APIs should not automatically trigger imports, which would be
1890+
surprising and potentially expensive. Real-world experience implementing lazy
1891+
imports in the standard library (such as the traceback module) showed that
1892+
automatic reification on ``__dict__`` access was cumbersome and forced
1893+
introspection code to load modules it was only examining.
1894+
1895+
Option 1 (always reifying) was rejected because it would make ``globals()``
1896+
and ``__dict__`` access surprisingly expensive and prevent introspecting the
1897+
lazy state of a module. Option 3 was initially considered to "protect" external
1898+
code from seeing lazy objects, but real-world usage showed this created more
1899+
problems than it solved, particularly for stdlib code that needs to introspect
1900+
modules without triggering side effects.
19201901

19211902
Acknowledgements
19221903
================

0 commit comments

Comments
 (0)