Skip to content

Commit b0db004

Browse files
committed
PEP 810: Update decisions on with blocks and __dict__ reification
This commit updates PEP 810 based on team discussion and feedback: 1. Allow lazy imports inside with blocks - Removed with blocks from syntax restrictions - Added rejected ideas section explaining why restriction was considered 2. __dict__ does not automatically reify lazy imports - Both globals() and __dict__ return raw dictionary without reification - Updated reification section, FAQ, and rejected ideas
1 parent bfffc49 commit b0db004

File tree

1 file changed

+128
-93
lines changed

1 file changed

+128
-93
lines changed

peps/pep-0810.rst

Lines changed: 128 additions & 93 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
464+
the object. Calling ``dir()`` at the global scope will not reify the globals
465+
and calling ``dir(mod)`` will be special cased in ``mod.__dir__`` to avoid
471466
reification as well.
472467

473-
Example using ``__dict__`` from external code:
474-
475-
.. code-block:: python
476-
477-
# my_module.py
478-
import sys
479-
lazy import json
480-
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'>
468+
However, calling ``globals()`` or accessing a module's ``__dict__`` does **not**
469+
trigger reification -- they return the module's dictionary, and accessing lazy
470+
objects through that dictionary still returns lazy proxy objects that need to
471+
be manually reified upon use. A lazy object can be resolved explicitly by
472+
calling the ``resolve`` method. Other, more indirect ways of accessing
473+
arbitrary globals (e.g. inspecting ``frame.f_globals``) also do **not** reify
474+
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,35 +1230,40 @@ 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()``
1256-
method.
1233+
Yes, individual lazy objects can be resolved by calling their ``resolve()``
1234+
method. Note that accessing a module's ``__dict__`` or calling ``globals()``
1235+
does **not** automatically reify lazy imports -- you'll see the lazy proxy
1236+
objects themselves, which you can then manually resolve if needed.
12571237

12581238
What's the difference between ``globals()`` and ``mod.__dict__`` for lazy imports?
12591239
----------------------------------------------------------------------------------
12601240

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:
1241+
Both ``globals()`` and ``mod.__dict__`` return the module's dictionary without
1242+
reifying lazy imports. Accessing lazy objects through either will yield lazy
1243+
proxy objects. This provides a consistent low-level API for introspection:
12651244

12661245
.. code-block:: python
12671246
12681247
# In your module:
12691248
lazy import json
12701249
12711250
g = globals()
1272-
print(type(g['json'])) # <class 'LazyImport'> - your problem
1251+
print(type(g['json'])) # <class 'LazyImport'>
1252+
1253+
d = __dict__
1254+
print(type(d['json'])) # <class 'LazyImport'>
12731255
12741256
# From external code:
12751257
import sys
12761258
mod = sys.modules['your_module']
12771259
d = mod.__dict__
1278-
print(type(d['json'])) # <class 'module'> - reified for external access
1260+
print(type(d['json'])) # <class 'LazyImport'>
12791261
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.
1262+
Both ``globals()`` and ``__dict__`` expose the raw namespace view without
1263+
implicit side effects. This symmetry makes the behavior predictable: accessing
1264+
the namespace dictionary never triggers imports. If you need to ensure an
1265+
import is resolved, call the ``resolve()`` method explicitly or access the
1266+
attribute directly (e.g., ``json.dumps``).
12831267

12841268
Why not use ``importlib.util.LazyLoader`` instead?
12851269
--------------------------------------------------
@@ -1664,6 +1648,59 @@ From the discussion on :pep:`690` it is clear that this is a fairly
16641648
contentious idea, although perhaps once we have wide-spread use of lazy
16651649
imports this can be reconsidered.
16661650

1651+
Disallowing lazy imports inside ``with`` blocks
1652+
------------------------------------------------
1653+
1654+
An earlier version of this PEP proposed disallowing ``lazy import`` statements
1655+
inside ``with`` blocks, similar to the restriction on ``try`` blocks. The
1656+
concern was that certain context managers (like ``contextlib.suppress(ImportError)``)
1657+
could suppress import errors in confusing ways when combined with lazy imports.
1658+
1659+
However, this restriction was rejected because ``with`` statements have much
1660+
broader semantics than ``try/except`` blocks. While ``try/except`` is explicitly
1661+
about catching exceptions, ``with`` blocks are commonly used for resource
1662+
management, temporary state changes, or scoping -- contexts where lazy imports
1663+
work perfectly fine. The ``lazy import`` syntax is explicit enough that
1664+
developers who write it inside a ``with`` block are making an intentional choice,
1665+
aligning with Python's "consenting adults" philosophy. For genuinely problematic
1666+
cases like ``with suppress(ImportError): lazy import foo``, static analysis
1667+
tools and linters are better suited to catch these patterns than hard language
1668+
restrictions.
1669+
1670+
Additionally, forbidding explicit ``lazy import`` in ``with`` blocks would
1671+
create complex rules for how the global lazy imports flag should behave,
1672+
leading to confusing inconsistencies between explicit and implicit laziness. By
1673+
allowing ``lazy import`` in ``with`` blocks, the rule is simple: the global
1674+
flag affects all module-level imports except those in ``try`` blocks and wild
1675+
card imports, matching exactly what's allowed with explicit syntax.
1676+
1677+
Forcing eager imports in ``with`` blocks under the global flag
1678+
---------------------------------------------------------------
1679+
1680+
Another rejected idea was to make imports inside ``with`` blocks remain eager
1681+
even when the global lazy imports flag is set to ``"all"``. The rationale was
1682+
to be conservative: since ``with`` statements can affect how imports behave
1683+
(e.g., by modifying ``sys.path`` or suppressing exceptions), forcing imports to
1684+
remain eager could prevent subtle bugs. However, this would create inconsistent
1685+
behavior where ``lazy import`` is allowed explicitly in ``with`` blocks, but
1686+
normal imports remain eager when the global flag is enabled. This inconsistency
1687+
between explicit and implicit laziness is confusing and hard to explain.
1688+
1689+
The simpler, more consistent rule is that the global flag affects imports
1690+
everywhere that explicit ``lazy import`` syntax is allowed. This avoids having
1691+
three different sets of rules (explicit syntax, global flag behavior, and filter
1692+
mechanism) and instead provides two: explicit syntax rules match what the global
1693+
flag affects, and the filter mechanism provides escape hatches for edge cases.
1694+
For users who need fine-grained control, the filter mechanism
1695+
(``sys.set_lazy_imports_filter()``) already provides a way to exclude specific
1696+
imports or patterns. Additionally, there's no inverse operation: if the global
1697+
flag forces imports eager in ``with`` blocks but a user wants them lazy, there's
1698+
no way to override it, creating an asymmetry.
1699+
1700+
In summary: imports in ``with`` blocks behave consistently whether marked
1701+
explicitly with ``lazy import`` or implicitly via the global flag, creating a
1702+
simple rule that's easy to explain and reason about.
1703+
16671704
Modification of the dict object
16681705
-------------------------------
16691706

@@ -1868,55 +1905,53 @@ from a real dict in almost all cases, which is extremely difficult to achieve
18681905
correctly. Any deviation from true dict behavior would be a source of subtle
18691906
bugs.
18701907

1871-
Reifying lazy imports when ``globals()`` is called
1872-
---------------------------------------------------
1908+
Automatically reifying on ``__dict__`` or ``globals()`` access
1909+
--------------------------------------------------------------
18731910

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:
1911+
Three options were considered for how ``globals()`` and ``mod.__dict__`` should
1912+
behave with lazy imports:
19071913

19081914
1. Calling ``globals()`` or ``mod.__dict__`` traverses and resolves all lazy
19091915
objects before returning.
19101916
2. Calling ``globals()`` or ``mod.__dict__`` returns the dictionary with lazy
1911-
objects present.
1917+
objects present (chosen).
19121918
3. Calling ``globals()`` returns the dictionary with lazy objects, but
19131919
``mod.__dict__`` reifies everything.
19141920

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.
1921+
We chose option 2: both ``globals()`` and ``__dict__`` return the raw
1922+
namespace dictionary without triggering reification. This provides a clean,
1923+
predictable model where low-level introspection APIs don't trigger side
1924+
effects.
1925+
1926+
**Reasons for this choice:**
1927+
1928+
**Consistency and symmetry**: Having ``globals()`` and ``__dict__`` behave
1929+
identically creates a simple mental model: both expose the raw namespace view.
1930+
This symmetry is easy to explain and predict. The alternative (option 3) would
1931+
create a confusing distinction where accessing the same dictionary through
1932+
different paths yields different behavior.
1933+
1934+
**Low-level APIs shouldn't trigger side effects**: Both ``globals()`` and
1935+
``__dict__`` are low-level introspection APIs. Making them automatically trigger
1936+
imports would be surprising and potentially expensive. Code that inspects
1937+
namespaces generally shouldn't cause module loading as a side effect.
1938+
1939+
**Real-world experience**: When implementing lazy imports in parts of the
1940+
standard library (such as the traceback module), it quickly became clear that
1941+
automatic reification on ``__dict__`` access was cumbersome and not the right
1942+
thing to do. Introspection code that needs to reason about module contents
1943+
shouldn't be forced into loading modules it's only examining.
1944+
1945+
**Explicit is better than implicit**: If code needs lazy imports resolved, it
1946+
can call ``resolve()`` explicitly or access the attributes directly. This puts
1947+
the control in the hands of the code that actually needs the imports loaded.
1948+
1949+
Option 1 (always reifying) was rejected because it would make ``globals()``
1950+
and ``__dict__`` access surprisingly expensive and would make it impossible to
1951+
introspect the lazy state of a module. Option 3 was initially considered to
1952+
"protect" external code from seeing lazy objects, but real-world usage showed
1953+
this created more problems than it solved, particularly for stdlib code that
1954+
needs to introspect modules without triggering side effects.
19201955

19211956
Acknowledgements
19221957
================

0 commit comments

Comments
 (0)