@@ -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,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
305337sequence 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
327359import statement: the import is *eager * (as if the lazy keyword was not used).
328360
329361Finally, 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
338369Lazy 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
603640The filter function is called for every potentially lazy import, and must
604641return ``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+
665706The 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
671712Where ``<mode> `` can be:
672713
@@ -687,12 +728,10 @@ lazy* import is ever imported lazily, the import filter is never called, and
687728the 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
691732the state of the global lazy imports flag inherited from the environment or CLI.
692733This 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
698737Backwards Compatibility
@@ -790,7 +829,7 @@ The `pyperformance suite`_ confirms the implementation is performance-neutral.
790829Filter 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
794833every *potentially lazy * import to determine whether it should actually be
795834lazy. When no filter is set, this is simply a NULL check (testing whether a
796835filter function has been registered), which is a highly predictable branch that
@@ -914,8 +953,8 @@ Security Implications
914953
915954There are no known security vulnerabilities introduced by lazy imports.
916955Security-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
919958control.
920959
921960How 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+
10231087What 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
12751339imported, and the fromlist (if using ``from ... import ``). Returning ``False ``
@@ -1638,18 +1702,36 @@ Making ``lazy`` imports find the module without loading it
16381702
16391703The Python ``import `` machinery separates out finding a module and loading
16401704it, 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
16541736Placing 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
17061788picture of real-world use and patterns, than it is to remove a hastily added
17071789mechanism 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+
17091808Using a decorator syntax for lazy imports
17101809------------------------------------------
17111810
0 commit comments