Skip to content

Commit cd78fac

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 - Only try blocks, functions, classes, and import * are prohibited - Global flag treats with blocks consistently (makes imports lazy) - 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 - Provides clean symmetry and predictable behavior - Low-level introspection APIs should not trigger side effects - Based on real-world experience with stdlib implementation - Updated reification section, FAQ, and rejected ideas The explicit lazy import syntax is sufficient for developers to understand what they're doing (consenting adults model). Linters can catch genuinely problematic cases. This creates simpler, more consistent rules between explicit and implicit (global flag) laziness.
1 parent bfffc49 commit cd78fac

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)