@@ -155,12 +155,15 @@ the brainstorming we have already completed in preparation for this proposal
155155as reference.
156156
157157The 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
165168Another important decision is to represent lazy imports with proxy objects in
166169the 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
285288loaded immediately at the import statement; instead, a lazy proxy object is
286289created and bound to the name. The actual module is loaded on first use of
287290that 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
305332sequence 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
327354import statement: the import is *eager * (as if the lazy keyword was not used).
328355
329356Finally, 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
338364Lazy objects
339365------------
@@ -586,19 +612,25 @@ 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 (see
618+ rejected ideas section for details). *
619+
620+ This PEP adds the following new functions to the ``sys `` module to manage the
621+ lazy imports filter:
590622
591- * ``importlib .set_lazy_imports_filter(func) `` - Sets the filter function. If
623+ * ``sys .set_lazy_imports_filter(func) `` - Sets the filter function. If
592624 ``func=None `` then the import filter is removed. The ``func `` parameter must
593625 have the signature: ``func(importer: str, name: str, fromlist: tuple[str, ...] | None) -> bool ``
594626
595- * ``importlib .get_lazy_imports_filter() `` - Returns the currently installed
627+ * ``sys .get_lazy_imports_filter() `` - Returns the currently installed
596628 filter function, or ``None `` if no filter is set.
597629
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).
630+ * ``sys .set_lazy_imports(mode , /) `` - Programmatic API for
631+ controlling lazy imports at runtime. The ``mode `` parameter can be
632+ ``"normal" `` (respect ``lazy `` keyword only), ``"all" `` (force all imports to be
633+ potentially lazy), or ``"none" `` (force all imports to be eager).
602634
603635The filter function is called for every potentially lazy import, and must
604636return ``True `` if the import should be lazy. This allows for fine-grained
@@ -646,7 +678,7 @@ Example:
646678 return True # Allow lazy import
647679
648680 # Install the filter
649- importlib .set_lazy_imports_filter(exclude_side_effect_modules)
681+ sys .set_lazy_imports_filter(exclude_side_effect_modules)
650682
651683 # These imports are checked by the filter
652684 lazy import data_processor # Filter returns True -> stays lazy
@@ -662,11 +694,15 @@ Example:
662694 Global lazy imports control
663695----------------------------
664696
697+ *Note: This is an advanced feature. Library developers should NOT use the global
698+ activation mechanism. This is intended for application developers and framework
699+ authors who need to control lazy imports across their entire application. *
700+
665701The global lazy imports flag can be controlled through:
666702
667703* The ``-X lazy_imports=<mode> `` command-line option
668704* The ``PYTHON_LAZY_IMPORTS=<mode> `` environment variable
669- * The ``importlib .set_lazy_imports(mode) `` function (primarily for testing)
705+ * The ``sys .set_lazy_imports(mode) `` function (primarily for testing)
670706
671707Where ``<mode> `` can be:
672708
@@ -687,12 +723,10 @@ lazy* import is ever imported lazily, the import filter is never called, and
687723the behavior is equivalent to a regular ``import `` statement: the import is
688724*eager * (as if the lazy keyword was not used).
689725
690- Python code can run the :func: `!importlib .set_lazy_imports ` function to override
726+ Python code can run the :func: `!sys .set_lazy_imports ` function to override
691727the state of the global lazy imports flag inherited from the environment or CLI.
692728This 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.
729+ are evaluated eagerly, via ``sys.set_lazy_imports("none") ``.
696730
697731
698732Backwards Compatibility
@@ -790,7 +824,7 @@ The `pyperformance suite`_ confirms the implementation is performance-neutral.
790824Filter function performance
791825~~~~~~~~~~~~~~~~~~~~~~~~~~~~
792826
793- The filter function (set via ``importlib .set_lazy_imports_filter() ``) is called for
827+ The filter function (set via ``sys .set_lazy_imports_filter() ``) is called for
794828every *potentially lazy * import to determine whether it should actually be
795829lazy. When no filter is set, this is simply a NULL check (testing whether a
796830filter function has been registered), which is a highly predictable branch that
@@ -914,8 +948,8 @@ Security Implications
914948
915949There are no known security vulnerabilities introduced by lazy imports.
916950Security-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
951+ can use :func: `!sys .set_lazy_imports ` with ``"none" `` to force
952+ eager evaluation, or use :func: `!sys .set_lazy_imports_filter ` for fine-grained
919953control.
920954
921955How to Teach This
@@ -1020,6 +1054,31 @@ global approach. The key differences are:
10201054- **Simpler implementation **: Uses proxy objects instead of modifying core
10211055 dictionary behavior.
10221056
1057+ What changes at reification time? What stays the same?
1058+ ------------------------------------------------------
1059+
1060+ **What changes (the timing): **
1061+
1062+ * **When ** the module is imported - deferred to first use instead of at the
1063+ import statement
1064+ * **When ** import errors occur - at first use rather than at import time
1065+ * **When ** module-level side effects execute - at first use rather than at
1066+ import time
1067+
1068+ **What stays the same (everything else): **
1069+
1070+ * The import machinery used - same ``__import__ ``, same hooks, same loaders
1071+ * The module object created - identical to an eagerly imported module
1072+ * Import state consulted - ``sys.path ``, ``sys.meta_path ``, etc. at
1073+ **reification time ** (not at import statement time)
1074+ * Module attributes and behavior - completely indistinguishable after
1075+ reification
1076+ * Thread safety - same import lock discipline as normal imports
1077+
1078+ In other words: lazy imports only change **when ** something happens, not
1079+ **what ** happens. After reification, a lazy-imported module is
1080+ indistinguishable from an eagerly imported one.
1081+
10231082What happens when lazy imports encounter errors?
10241083------------------------------------------------
10251084
@@ -1269,7 +1328,7 @@ exclude specific modules that are known to have problematic side effects:
12691328 return False # Import eagerly
12701329 return True # Allow lazy import
12711330
1272- importlib .set_lazy_imports_filter(my_filter)
1331+ sys .set_lazy_imports_filter(my_filter)
12731332
12741333 The filter function receives the importer module name, the module being
12751334imported, and the fromlist (if using ``from ... import ``). Returning ``False ``
@@ -1638,18 +1697,36 @@ Making ``lazy`` imports find the module without loading it
16381697
16391698The Python ``import `` machinery separates out finding a module and loading
16401699it, 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).
1700+ loading part. However, this approach was rejected for several critical reasons.
1701+
1702+ A significant part of the performance win comes from skipping the finding phase.
1703+ The issue is particularly acute on NFS-backed filesystems and distributed
1704+ storage, where each ``stat() `` call incurs network latency. In these kinds of
1705+ environments, ``stat() `` calls can take tens to hundreds of milliseconds
1706+ depending on network conditions. With dozens of imports each doing multiple
1707+ filesystem checks traversing ``sys.path ``, the time spent finding modules
1708+ before executing any Python code can become substantial. In some measurements,
1709+ spec finding accounts for the majority of total import time. Skipping only the
1710+ loading phase would leave most of the performance problem unsolved.
1711+
1712+ More critically, separating finding from loading creates the worst of both
1713+ worlds for error handling. Some exceptions from the import machinery (e.g.,
1714+ ``ImportError `` from a missing module, path resolution failures,
1715+ ``ModuleNotFoundError ``) would be raised at the ``lazy import `` statement, while
1716+ others (e.g., ``SyntaxError ``, ``ImportError `` from circular imports, attribute
1717+ errors from ``from module import name ``) would be raised later at first use.
1718+ This split is both confusing and unpredictable: developers would need to
1719+ understand the internal import machinery to know which errors happen when. The
1720+ current design is simpler: with full lazy imports, all import-related errors
1721+ occur at first use, making the behavior consistent and predictable.
1722+
1723+ Additionally, there are technical limitations: finding the module does not
1724+ guarantee the import will succeed, nor even that it will not raise ImportError.
1725+ Finding modules in packages requires that those packages are loaded, so it
1726+ would only help with lazy loading one level of a package hierarchy. Since
1727+ "finding" attributes in modules *requires * loading them, this would create a
1728+ hard to explain difference between ``from package import module `` and
1729+ ``from module import function ``.
16531730
16541731Placing the ``lazy `` keyword in the middle of from imports
16551732----------------------------------------------------------
@@ -1706,6 +1783,23 @@ to add more specific re-enabling mechanisms later, when we have a clearer
17061783picture of real-world use and patterns, than it is to remove a hastily added
17071784mechanism that isn't quite right.
17081785
1786+ Using underscore-prefixed names for advanced features
1787+ ------------------------------------------------------
1788+
1789+ The global activation and filter functions (``sys.set_lazy_imports ``,
1790+ ``sys.set_lazy_imports_filter ``, ``sys.get_lazy_imports_filter ``) could be
1791+ marked as "private" or "advanced" by using underscore prefixes (e.g.,
1792+ ``sys._set_lazy_imports_filter ``). This was rejected because branding as
1793+ advanced features through documentation is sufficient. These functions have
1794+ legitimate use cases for advanced users, particularly operators of large
1795+ deployments. Providing an official mechanism prevents divergence from upstream
1796+ CPython. The global mode is intentionally documented as an advanced feature for
1797+ operators running huge fleets, not for day-to-day users or libraries. Python
1798+ has precedent for advanced features that remain public APIs without underscore
1799+ prefixes - for example, ``gc.disable() ``, ``gc.get_objects() ``, and
1800+ ``gc.set_threshold() `` are advanced features that can cause issues if misused,
1801+ yet they are not underscore-prefixed.
1802+
17091803Using a decorator syntax for lazy imports
17101804------------------------------------------
17111805
0 commit comments