Skip to content

Commit e893293

Browse files
committed
PEP 810: Updates from discussion feedback
1 parent 2ae743e commit e893293

File tree

1 file changed

+138
-45
lines changed

1 file changed

+138
-45
lines changed

peps/pep-0810.rst

Lines changed: 138 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -155,12 +155,15 @@ the brainstorming we have already completed in preparation for this proposal
155155
as reference.
156156

157157
The choice to introduce a new ``lazy`` keyword reflects the need for explicit
158-
syntax. Import behavior is too fundamental to be left implicit or hidden
159-
behind global flags or environment variables. By marking laziness directly at
160-
the import site, the intent is immediately visible to both readers and tools.
161-
This avoids surprises, reduces the cognitive burden of reasoning about
162-
imports, and keeps lazy import semantics in line with Python's tradition of
163-
explicitness.
158+
syntax. Lazy imports have different semantics from normal imports: errors and
159+
side effects occur at first use rather than at the import statement. This
160+
semantic difference makes it critical that laziness is visible at the import
161+
site itself, not hidden in global configuration or distant module-level
162+
declarations. The ``lazy`` keyword provides local reasoning about import
163+
behavior, avoiding the need to search elsewhere in the code to understand
164+
whether an import is deferred. The rest of the import semantics remain
165+
unchanged: the same import machinery, module finding, and loading mechanisms
166+
are used.
164167

165168
Another important decision is to represent lazy imports with proxy objects in
166169
the module's namespace, rather than by modifying dictionary lookup. Earlier
@@ -280,13 +283,19 @@ Examples of syntax errors:
280283
Semantics
281284
---------
282285

283-
When the ``lazy`` keyword is used, the import becomes *potentially lazy*.
284-
Unless lazy imports are disabled or suppressed (see below), the module is not
286+
When the ``lazy`` keyword is used, the import becomes *potentially lazy*
287+
(see `Lazy imports filter`_ for advanced override mechanisms). The module is not
285288
loaded immediately at the import statement; instead, a lazy proxy object is
286289
created and bound to the name. The actual module is loaded on first use of
287290
that name.
288291

289-
Example:
292+
When using ``lazy from ... import``, **each imported name** is bound to a lazy
293+
proxy object. The first access to **any** of these names triggers loading of
294+
the entire module and reifies **only that specific name** to its actual value.
295+
Other names remain as lazy proxies until they are accessed. The interpreter's
296+
adaptive specialization will optimize away the lazy checks after a few accesses.
297+
298+
Example with ``lazy import``:
290299

291300
.. code-block:: python
292301
@@ -301,6 +310,24 @@ Example:
301310
302311
print('json' in sys.modules) # True - now loaded
303312
313+
Example with ``lazy from ... import``:
314+
315+
.. code-block:: python
316+
317+
import sys
318+
319+
lazy from json import dumps, loads
320+
321+
print('json' in sys.modules) # False - module not loaded yet
322+
323+
# First use of 'dumps' triggers loading json and reifies ONLY 'dumps'
324+
result = dumps({"hello": "world"})
325+
326+
print('json' in sys.modules) # True - module now loaded
327+
328+
# Accessing 'loads' now reifies it (json already loaded, no re-import)
329+
data = loads(result)
330+
304331
A module may contain a :data:`!__lazy_modules__` attribute, which is a
305332
sequence of fully qualified module names (strings) to make *potentially lazy*
306333
(as if the ``lazy`` keyword was used). This attribute is checked on each
@@ -327,13 +354,12 @@ import is ever imported lazily, and the behavior is equivalent to a regular
327354
import statement: the import is *eager* (as if the lazy keyword was not used).
328355

329356
Finally, the application may use a custom filter function on all *potentially
330-
lazy* imports to determine if they should be lazy or not.
331-
If a filter function is set, it will be called with the name of the module
332-
doing the import, the name of the module being imported, and (if applicable)
333-
the fromlist.
334-
An import remains lazy only if the filter function returns ``True``.
335-
336-
If no lazy import filter is set, all *potentially lazy* imports are lazy.
357+
lazy* imports to determine if they should be lazy or not (this is an advanced
358+
feature, see `Lazy imports filter`_). If a filter function is set, it will be
359+
called with the name of the module doing the import, the name of the module
360+
being imported, and (if applicable) the fromlist. An import remains lazy only
361+
if the filter function returns ``True``. If no lazy import filter is set, all
362+
*potentially lazy* imports are lazy.
337363

338364
Lazy objects
339365
------------
@@ -586,19 +612,24 @@ After several calls, ``LOAD_GLOBAL`` specializes to ``LOAD_GLOBAL_MODULE``:
586612
Lazy imports filter
587613
-------------------
588614

589-
This PEP adds the following new functions to manage the lazy imports filter:
615+
*Note: This is an advanced feature. Library developers should NOT call these
616+
functions. These are intended for specialized/advanced users who need
617+
fine-grained control over lazy import behavior when using the global flags.*
618+
619+
This PEP adds the following new functions to the ``sys`` module to manage the
620+
lazy imports filter:
590621

591-
* ``importlib.set_lazy_imports_filter(func)`` - Sets the filter function. If
622+
* ``sys.set_lazy_imports_filter(func)`` - Sets the filter function. If
592623
``func=None`` then the import filter is removed. The ``func`` parameter must
593624
have the signature: ``func(importer: str, name: str, fromlist: tuple[str, ...] | None) -> bool``
594625

595-
* ``importlib.get_lazy_imports_filter()`` - Returns the currently installed
626+
* ``sys.get_lazy_imports_filter()`` - Returns the currently installed
596627
filter function, or ``None`` if no filter is set.
597628

598-
* ``importlib.set_lazy_imports(enabled=None, /)`` - Programmatic API for
599-
controlling lazy imports at runtime. The ``enabled`` parameter can be
600-
``None`` (respect ``lazy`` keyword only), ``True`` (force all imports to be
601-
potentially lazy), or ``False`` (force all imports to be eager).
629+
* ``sys.set_lazy_imports(mode, /)`` - Programmatic API for
630+
controlling lazy imports at runtime. The ``mode`` parameter can be
631+
``"normal"`` (respect ``lazy`` keyword only), ``"all"`` (force all imports to be
632+
potentially lazy), or ``"none"`` (force all imports to be eager).
602633

603634
The filter function is called for every potentially lazy import, and must
604635
return ``True`` if the import should be lazy. This allows for fine-grained
@@ -646,7 +677,7 @@ Example:
646677
return True # Allow lazy import
647678
648679
# Install the filter
649-
importlib.set_lazy_imports_filter(exclude_side_effect_modules)
680+
sys.set_lazy_imports_filter(exclude_side_effect_modules)
650681
651682
# These imports are checked by the filter
652683
lazy import data_processor # Filter returns True -> stays lazy
@@ -662,11 +693,15 @@ Example:
662693
Global lazy imports control
663694
----------------------------
664695

696+
*Note: This is an advanced feature. Library developers should NOT use the global
697+
activation mechanism. This is intended for application developers and framework
698+
authors who need to control lazy imports across their entire application.*
699+
665700
The global lazy imports flag can be controlled through:
666701

667702
* The ``-X lazy_imports=<mode>`` command-line option
668703
* The ``PYTHON_LAZY_IMPORTS=<mode>`` environment variable
669-
* The ``importlib.set_lazy_imports(mode)`` function (primarily for testing)
704+
* The ``sys.set_lazy_imports(mode)`` function (primarily for testing)
670705

671706
Where ``<mode>`` can be:
672707

@@ -687,12 +722,10 @@ lazy* import is ever imported lazily, the import filter is never called, and
687722
the behavior is equivalent to a regular ``import`` statement: the import is
688723
*eager* (as if the lazy keyword was not used).
689724

690-
Python code can run the :func:`!importlib.set_lazy_imports` function to override
725+
Python code can run the :func:`!sys.set_lazy_imports` function to override
691726
the state of the global lazy imports flag inherited from the environment or CLI.
692727
This is especially useful if an application needs to ensure that all imports
693-
are evaluated eagerly, via ``importlib.set_lazy_imports('none')``.
694-
Alternatively, :func:`!importlib.set_lazy_imports` can be used with boolean
695-
values for programmatic control.
728+
are evaluated eagerly, via ``sys.set_lazy_imports("none")``.
696729

697730

698731
Backwards Compatibility
@@ -790,7 +823,7 @@ The `pyperformance suite`_ confirms the implementation is performance-neutral.
790823
Filter function performance
791824
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
792825

793-
The filter function (set via ``importlib.set_lazy_imports_filter()``) is called for
826+
The filter function (set via ``sys.set_lazy_imports_filter()``) is called for
794827
every *potentially lazy* import to determine whether it should actually be
795828
lazy. When no filter is set, this is simply a NULL check (testing whether a
796829
filter function has been registered), which is a highly predictable branch that
@@ -914,8 +947,8 @@ Security Implications
914947

915948
There are no known security vulnerabilities introduced by lazy imports.
916949
Security-sensitive tools that need to ensure all imports are evaluated eagerly
917-
can use :func:`!importlib.set_lazy_imports` with ``enabled=False`` to force
918-
eager evaluation, or use :func:`!importlib.set_lazy_imports_filter` for fine-grained
950+
can use :func:`!sys.set_lazy_imports` with ``"none"`` to force
951+
eager evaluation, or use :func:`!sys.set_lazy_imports_filter` for fine-grained
919952
control.
920953

921954
How to Teach This
@@ -1020,6 +1053,31 @@ global approach. The key differences are:
10201053
- **Simpler implementation**: Uses proxy objects instead of modifying core
10211054
dictionary behavior.
10221055

1056+
What changes at reification time? What stays the same?
1057+
------------------------------------------------------
1058+
1059+
**What changes (the timing):**
1060+
1061+
* **When** the module is imported - deferred to first use instead of at the
1062+
import statement
1063+
* **When** import errors occur - at first use rather than at import time
1064+
* **When** module-level side effects execute - at first use rather than at
1065+
import time
1066+
1067+
**What stays the same (everything else):**
1068+
1069+
* The import machinery used - same ``__import__``, same hooks, same loaders
1070+
* The module object created - identical to an eagerly imported module
1071+
* Import state consulted - ``sys.path``, ``sys.meta_path``, etc. at
1072+
**reification time** (not at import statement time)
1073+
* Module attributes and behavior - completely indistinguishable after
1074+
reification
1075+
* Thread safety - same import lock discipline as normal imports
1076+
1077+
In other words: lazy imports only change **when** something happens, not
1078+
**what** happens. After reification, a lazy-imported module is
1079+
indistinguishable from an eagerly imported one.
1080+
10231081
What happens when lazy imports encounter errors?
10241082
------------------------------------------------
10251083

@@ -1269,7 +1327,7 @@ exclude specific modules that are known to have problematic side effects:
12691327
return False # Import eagerly
12701328
return True # Allow lazy import
12711329
1272-
importlib.set_lazy_imports_filter(my_filter)
1330+
sys.set_lazy_imports_filter(my_filter)
12731331
12741332
The filter function receives the importer module name, the module being
12751333
imported, and the fromlist (if using ``from ... import``). Returning ``False``
@@ -1638,18 +1696,36 @@ Making ``lazy`` imports find the module without loading it
16381696

16391697
The Python ``import`` machinery separates out finding a module and loading
16401698
it, and the lazy import implementation could technically defer only the
1641-
loading part. However:
1642-
1643-
- Finding the module does not guarantee the import will succeed, nor even
1644-
that it will not raise ImportError.
1645-
- Finding modules in packages requires that those packages are loaded, so
1646-
it would only help with lazy loading one level of a package hierarchy.
1647-
- Since "finding" attributes in modules *requires* loading them, this would
1648-
create a hard to explain difference between
1649-
``from package import module`` and ``from module import function``.
1650-
- A significant part of the performance win is skipping the finding part
1651-
(which may involve filesystem searches and consulting multiple importers
1652-
and meta-importers).
1699+
loading part. However, this approach was rejected for several critical reasons.
1700+
1701+
A significant part of the performance win comes from skipping the finding phase.
1702+
The issue is particularly acute on NFS-backed filesystems and distributed
1703+
storage, where each ``stat()`` call incurs network latency. In these kinds of
1704+
environments, ``stat()`` calls can take tens to hundreds of milliseconds
1705+
depending on network conditions. With dozens of imports each doing multiple
1706+
filesystem checks traversing ``sys.path``, the time spent finding modules
1707+
before executing any Python code can become substantial. In some measurements,
1708+
spec finding accounts for the majority of total import time. Skipping only the
1709+
loading phase would leave most of the performance problem unsolved.
1710+
1711+
More critically, separating finding from loading creates the worst of both
1712+
worlds for error handling. Some exceptions from the import machinery (e.g.,
1713+
``ImportError`` from a missing module, path resolution failures,
1714+
``ModuleNotFoundError``) would be raised at the ``lazy import`` statement, while
1715+
others (e.g., ``SyntaxError``, ``ImportError`` from circular imports, attribute
1716+
errors from ``from module import name``) would be raised later at first use.
1717+
This split is both confusing and unpredictable: developers would need to
1718+
understand the internal import machinery to know which errors happen when. The
1719+
current design is simpler: with full lazy imports, all import-related errors
1720+
occur at first use, making the behavior consistent and predictable.
1721+
1722+
Additionally, there are technical limitations: finding the module does not
1723+
guarantee the import will succeed, nor even that it will not raise ImportError.
1724+
Finding modules in packages requires that those packages are loaded, so it
1725+
would only help with lazy loading one level of a package hierarchy. Since
1726+
"finding" attributes in modules *requires* loading them, this would create a
1727+
hard to explain difference between ``from package import module`` and
1728+
``from module import function``.
16531729

16541730
Placing the ``lazy`` keyword in the middle of from imports
16551731
----------------------------------------------------------
@@ -1706,6 +1782,23 @@ to add more specific re-enabling mechanisms later, when we have a clearer
17061782
picture of real-world use and patterns, than it is to remove a hastily added
17071783
mechanism that isn't quite right.
17081784

1785+
Using underscore-prefixed names for advanced features
1786+
------------------------------------------------------
1787+
1788+
The global activation and filter functions (``sys.set_lazy_imports``,
1789+
``sys.set_lazy_imports_filter``, ``sys.get_lazy_imports_filter``) could be
1790+
marked as "private" or "advanced" by using underscore prefixes (e.g.,
1791+
``sys._set_lazy_imports_filter``). This was rejected because branding as
1792+
advanced features through documentation is sufficient. These functions have
1793+
legitimate use cases for advanced users, particularly operators of large
1794+
deployments. Providing an official mechanism prevents divergence from upstream
1795+
CPython. The global mode is intentionally documented as an advanced feature for
1796+
operators running huge fleets, not for day-to-day users or libraries. Python
1797+
has precedent for advanced features that remain public APIs without underscore
1798+
prefixes - for example, ``gc.disable()``, ``gc.get_objects()``, and
1799+
``gc.set_threshold()`` are advanced features that can cause issues if misused,
1800+
yet they are not underscore-prefixed.
1801+
17091802
Using a decorator syntax for lazy imports
17101803
------------------------------------------
17111804

0 commit comments

Comments
 (0)