Skip to content

Commit 790fcd4

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

File tree

1 file changed

+144
-45
lines changed

1 file changed

+144
-45
lines changed

peps/pep-0810.rst

Lines changed: 144 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,29 @@ 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+
print(type(globals()['dumps'])) # <class 'LazyImport'> - proxy object
323+
print(type(globals()['loads'])) # <class 'LazyImport'> - proxy object
324+
325+
# First use of 'dumps' triggers loading json and reifies ONLY 'dumps'
326+
result = dumps({"hello": "world"})
327+
328+
print('json' in sys.modules) # True - module now loaded
329+
print(type(globals()['dumps'])) # <class 'builtin_function_or_method'> - reified
330+
print(type(globals()['loads'])) # <class 'LazyImport'> - still a proxy!
331+
332+
# Accessing 'loads' now reifies it (json already loaded, no re-import)
333+
data = loads(result)
334+
print(type(globals()['loads'])) # <class 'builtin_function_or_method'> - reified
335+
304336
A module may contain a :data:`!__lazy_modules__` attribute, which is a
305337
sequence of fully qualified module names (strings) to make *potentially lazy*
306338
(as if the ``lazy`` keyword was used). This attribute is checked on each
@@ -327,13 +359,12 @@ import is ever imported lazily, and the behavior is equivalent to a regular
327359
import statement: the import is *eager* (as if the lazy keyword was not used).
328360

329361
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.
362+
lazy* imports to determine if they should be lazy or not (this is an advanced
363+
feature, see `Lazy imports filter`_). If a filter function is set, it will be
364+
called with the name of the module doing the import, the name of the module
365+
being imported, and (if applicable) the fromlist. An import remains lazy only
366+
if the filter function returns ``True``. If no lazy import filter is set, all
367+
*potentially lazy* imports are lazy.
337368

338369
Lazy objects
339370
------------
@@ -586,19 +617,25 @@ After several calls, ``LOAD_GLOBAL`` specializes to ``LOAD_GLOBAL_MODULE``:
586617
Lazy imports filter
587618
-------------------
588619

589-
This PEP adds the following new functions to manage the lazy imports filter:
620+
*Note: This is an advanced feature. Library developers should NOT call these
621+
functions. These are intended for specialized/advanced users who need
622+
fine-grained control over lazy import behavior when using the global flags (see
623+
rejected ideas section for details).*
624+
625+
This PEP adds the following new functions to the ``sys`` module to manage the
626+
lazy imports filter:
590627

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

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

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).
635+
* ``sys.set_lazy_imports(mode, /)`` - Programmatic API for
636+
controlling lazy imports at runtime. The ``mode`` parameter can be
637+
``"normal"`` (respect ``lazy`` keyword only), ``"all"`` (force all imports to be
638+
potentially lazy), or ``"none"`` (force all imports to be eager).
602639

603640
The filter function is called for every potentially lazy import, and must
604641
return ``True`` if the import should be lazy. This allows for fine-grained
@@ -646,7 +683,7 @@ Example:
646683
return True # Allow lazy import
647684
648685
# Install the filter
649-
importlib.set_lazy_imports_filter(exclude_side_effect_modules)
686+
sys.set_lazy_imports_filter(exclude_side_effect_modules)
650687
651688
# These imports are checked by the filter
652689
lazy import data_processor # Filter returns True -> stays lazy
@@ -662,11 +699,15 @@ Example:
662699
Global lazy imports control
663700
----------------------------
664701

702+
*Note: This is an advanced feature. Library developers should NOT use the global
703+
activation mechanism. This is intended for application developers and framework
704+
authors who need to control lazy imports across their entire application.*
705+
665706
The global lazy imports flag can be controlled through:
666707

667708
* The ``-X lazy_imports=<mode>`` command-line option
668709
* The ``PYTHON_LAZY_IMPORTS=<mode>`` environment variable
669-
* The ``importlib.set_lazy_imports(mode)`` function (primarily for testing)
710+
* The ``sys.set_lazy_imports(mode)`` function (primarily for testing)
670711

671712
Where ``<mode>`` can be:
672713

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

690-
Python code can run the :func:`!importlib.set_lazy_imports` function to override
731+
Python code can run the :func:`!sys.set_lazy_imports` function to override
691732
the state of the global lazy imports flag inherited from the environment or CLI.
692733
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.
734+
are evaluated eagerly, via ``sys.set_lazy_imports("none")``.
696735

697736

698737
Backwards Compatibility
@@ -790,7 +829,7 @@ The `pyperformance suite`_ confirms the implementation is performance-neutral.
790829
Filter function performance
791830
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
792831

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

915954
There are no known security vulnerabilities introduced by lazy imports.
916955
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
956+
can use :func:`!sys.set_lazy_imports` with ``"none"`` to force
957+
eager evaluation, or use :func:`!sys.set_lazy_imports_filter` for fine-grained
919958
control.
920959

921960
How to Teach This
@@ -1020,6 +1059,31 @@ global approach. The key differences are:
10201059
- **Simpler implementation**: Uses proxy objects instead of modifying core
10211060
dictionary behavior.
10221061

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

@@ -1269,7 +1333,7 @@ exclude specific modules that are known to have problematic side effects:
12691333
return False # Import eagerly
12701334
return True # Allow lazy import
12711335
1272-
importlib.set_lazy_imports_filter(my_filter)
1336+
sys.set_lazy_imports_filter(my_filter)
12731337
12741338
The filter function receives the importer module name, the module being
12751339
imported, and the fromlist (if using ``from ... import``). Returning ``False``
@@ -1638,18 +1702,36 @@ Making ``lazy`` imports find the module without loading it
16381702

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

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

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

0 commit comments

Comments
 (0)