From ba8fd8a74b2c89eb4d663c8ae1d0d406e8715f54 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Tue, 5 May 2026 00:08:46 +0200 Subject: [PATCH 1/6] pep-0810: Document sys.lazy_modules as immutable frozendict snapshot Clarify the public introspection contract for ``sys.lazy_modules``: it is a read-only ``frozendict[str, frozenset[str]]`` snapshot rebuilt on each access (via ``sys.__getattr__``), not a live mutable set or dict, so user code cannot mutate the import system's bookkeeping. Add a "Public introspection API" subsection alongside the existing filter API listing, and update prose to reflect the snapshot-on-access contract under "Lazy import mechanism", "Reification", and the subinterpreter note. --- peps/pep-0810.rst | 39 ++++++++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/peps/pep-0810.rst b/peps/pep-0810.rst index db84fa16f80..a3223d3c37a 100644 --- a/peps/pep-0810.rst +++ b/peps/pep-0810.rst @@ -385,9 +385,8 @@ Lazy import mechanism When an import is lazy, ``__lazy_import__`` is called instead of ``__import__``. ``__lazy_import__`` has the same function signature as -``__import__``. It adds the module name to ``sys.lazy_modules``, a set of -fully-qualified module names which have been lazily imported at some point -(primarily for diagnostics and introspection), and returns a +``__import__``. It records the lazy import in the registry exposed (read +only) as :data:`!sys.lazy_modules`, and returns a :class:`!types.LazyImportType` object for the module. The implementation of ``from ... import`` (the ``IMPORT_FROM`` bytecode @@ -398,11 +397,12 @@ instead. The end result of this process is that lazy imports (regardless of how they are enabled) result in lazy objects being assigned to global variables. -Lazy module objects do not appear in ``sys.modules``, they're just listed in -the ``sys.lazy_modules`` set. Under normal operation lazy objects should only -end up stored in global variables, and the common ways to access those -variables (regular variable access, module attributes) will resolve lazy -imports (reify) and replace them when they're accessed. +Lazy module objects do not appear in ``sys.modules``; they are visible via +:data:`!sys.lazy_modules` (see `Public introspection API`_ below). Under +normal operation lazy objects should only end up stored in global variables, +and the common ways to access those variables (regular variable access, +module attributes) will resolve lazy imports (reify) and replace them when +they're accessed. It is still possible to expose lazy objects through other means, like debuggers. This is not considered a problem. @@ -418,8 +418,9 @@ uses the state of the import system (e.g. ``sys.path``, ``sys.meta_path``, ``sys.path_hooks`` and ``__import__``) at **reification** time, **not** the state when the ``lazy import`` statement was evaluated. -When the module is reified, it's removed from ``sys.lazy_modules`` (even if -there are still other unreified lazy references to it). When a package is +When the module is reified, it's removed from the registry tracked by +:data:`!sys.lazy_modules` (even if there are still other unreified lazy +references to it). When a package is reified and submodules in the package were also previously lazily imported, those submodules are *not* automatically reified but they *are* added to the reified package's globals (unless the package already assigned something @@ -630,6 +631,17 @@ lazy imports filter: * ``sys.get_lazy_imports()`` - Returns the current lazy imports mode as a string: ``"normal"``, ``"all"``, or ``"none"``. +Public introspection API +~~~~~~~~~~~~~~~~~~~~~~~~ + +* :data:`!sys.lazy_modules` - An immutable + ``frozendict[str, frozenset[str]]`` snapshot of the lazy import registry, + mapping each module name to the set of its lazily-imported submodule + names. The attribute is rebuilt on each access (via + :meth:`!sys.__getattr__`); it is a read-only view, not the live registry, + so user code cannot mutate the import system's bookkeeping. Primarily + intended for diagnostics and introspection. + The filter function is called for every potentially lazy import, and must return ``True`` if the import should be lazy. This allows for fine-grained control over which imports should be lazy, useful for excluding modules with @@ -801,9 +813,10 @@ may be imported in a different thread if that thread triggers the first access to the lazy import. This is not a problem: the import lock ensures thread safety regardless of which thread performs the import. -Subinterpreters are supported. Each subinterpreter maintains its own -``sys.lazy_modules`` and import state, so lazy imports in one subinterpreter -do not affect others. +Subinterpreters are supported. Each subinterpreter maintains its own lazy +import registry (and therefore its own :data:`!sys.lazy_modules` snapshot) +and import state, so lazy imports in one subinterpreter do not affect +others. Performance ----------- From bce574ca1acd5dc0360d5cb7643566b819a51e40 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Tue, 5 May 2026 00:38:32 +0200 Subject: [PATCH 2/6] pep-0810: Trim sys.lazy_modules to type-only contract Prior commit added a "Public introspection API" subsection plus prose about snapshot semantics, the registry, and sys.__getattr__ rebuild behavior. That overshoots the contract: callers only need to know the type and that it is read-only. Fold the corrected type (``frozendict[str, frozenset[str]]``) into the existing first mention under "Lazy import mechanism", drop the new subsection, and remove the imprecise claim that lazy proxies are "listed in" ``sys.lazy_modules`` (the proxies live in globals; the mapping records each module's lazy imports). --- peps/pep-0810.rst | 42 +++++++++++++++--------------------------- 1 file changed, 15 insertions(+), 27 deletions(-) diff --git a/peps/pep-0810.rst b/peps/pep-0810.rst index a3223d3c37a..a000c3f1168 100644 --- a/peps/pep-0810.rst +++ b/peps/pep-0810.rst @@ -385,9 +385,11 @@ Lazy import mechanism When an import is lazy, ``__lazy_import__`` is called instead of ``__import__``. ``__lazy_import__`` has the same function signature as -``__import__``. It records the lazy import in the registry exposed (read -only) as :data:`!sys.lazy_modules`, and returns a -:class:`!types.LazyImportType` object for the module. +``__import__``. It records the lazy import in ``sys.lazy_modules``, a +read-only mapping from each module's name to the (frozen) set of its +lazily-imported submodule names (primarily for diagnostics and +introspection), and returns a :class:`!types.LazyImportType` object for +the module. The implementation of ``from ... import`` (the ``IMPORT_FROM`` bytecode implementation) checks if the module it's fetching from is a lazy module @@ -397,12 +399,11 @@ instead. The end result of this process is that lazy imports (regardless of how they are enabled) result in lazy objects being assigned to global variables. -Lazy module objects do not appear in ``sys.modules``; they are visible via -:data:`!sys.lazy_modules` (see `Public introspection API`_ below). Under -normal operation lazy objects should only end up stored in global variables, -and the common ways to access those variables (regular variable access, -module attributes) will resolve lazy imports (reify) and replace them when -they're accessed. +Lazy module objects do not appear in ``sys.modules``. Under normal +operation lazy objects should only end up stored in global variables, and +the common ways to access those variables (regular variable access, module +attributes) will resolve lazy imports (reify) and replace them when they're +accessed. It is still possible to expose lazy objects through other means, like debuggers. This is not considered a problem. @@ -418,9 +419,8 @@ uses the state of the import system (e.g. ``sys.path``, ``sys.meta_path``, ``sys.path_hooks`` and ``__import__``) at **reification** time, **not** the state when the ``lazy import`` statement was evaluated. -When the module is reified, it's removed from the registry tracked by -:data:`!sys.lazy_modules` (even if there are still other unreified lazy -references to it). When a package is +When the module is reified, it's removed from ``sys.lazy_modules`` (even if +there are still other unreified lazy references to it). When a package is reified and submodules in the package were also previously lazily imported, those submodules are *not* automatically reified but they *are* added to the reified package's globals (unless the package already assigned something @@ -631,17 +631,6 @@ lazy imports filter: * ``sys.get_lazy_imports()`` - Returns the current lazy imports mode as a string: ``"normal"``, ``"all"``, or ``"none"``. -Public introspection API -~~~~~~~~~~~~~~~~~~~~~~~~ - -* :data:`!sys.lazy_modules` - An immutable - ``frozendict[str, frozenset[str]]`` snapshot of the lazy import registry, - mapping each module name to the set of its lazily-imported submodule - names. The attribute is rebuilt on each access (via - :meth:`!sys.__getattr__`); it is a read-only view, not the live registry, - so user code cannot mutate the import system's bookkeeping. Primarily - intended for diagnostics and introspection. - The filter function is called for every potentially lazy import, and must return ``True`` if the import should be lazy. This allows for fine-grained control over which imports should be lazy, useful for excluding modules with @@ -813,10 +802,9 @@ may be imported in a different thread if that thread triggers the first access to the lazy import. This is not a problem: the import lock ensures thread safety regardless of which thread performs the import. -Subinterpreters are supported. Each subinterpreter maintains its own lazy -import registry (and therefore its own :data:`!sys.lazy_modules` snapshot) -and import state, so lazy imports in one subinterpreter do not affect -others. +Subinterpreters are supported. Each subinterpreter maintains its own +``sys.lazy_modules`` and import state, so lazy imports in one subinterpreter +do not affect others. Performance ----------- From 420845afc5ce18f3f550f11db75f2b74920e1724 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Tue, 5 May 2026 01:00:45 +0200 Subject: [PATCH 3/6] Clarify what happens when module is reified --- peps/pep-0810.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/peps/pep-0810.rst b/peps/pep-0810.rst index a000c3f1168..f1289411568 100644 --- a/peps/pep-0810.rst +++ b/peps/pep-0810.rst @@ -419,12 +419,12 @@ uses the state of the import system (e.g. ``sys.path``, ``sys.meta_path``, ``sys.path_hooks`` and ``__import__``) at **reification** time, **not** the state when the ``lazy import`` statement was evaluated. -When the module is reified, it's removed from ``sys.lazy_modules`` (even if -there are still other unreified lazy references to it). When a package is -reified and submodules in the package were also previously lazily imported, -those submodules are *not* automatically reified but they *are* added to the -reified package's globals (unless the package already assigned something -else to the name of the submodule). +When the module is reified, its fully qualified module name is removed from +``sys.lazy_modules`` (even if there are still other unreified lazy references +to it). When a package is reified and submodules in the package were also +previously lazily imported, those submodules are *not* automatically reified +but they *are* added to the reified package's globals (unless the package +already assigned something else to the name of the submodule). If reification fails (e.g., due to an ``ImportError``), the lazy object is *not* reified or replaced. Subsequent uses of the lazy object will re-try From 0b0fbd0f82278d519a73eb37a2dce49eb63d4772 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Tue, 5 May 2026 01:02:48 +0200 Subject: [PATCH 4/6] Fix redundancy --- peps/pep-0810.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/peps/pep-0810.rst b/peps/pep-0810.rst index f1289411568..87a0a0447b9 100644 --- a/peps/pep-0810.rst +++ b/peps/pep-0810.rst @@ -419,7 +419,7 @@ uses the state of the import system (e.g. ``sys.path``, ``sys.meta_path``, ``sys.path_hooks`` and ``__import__``) at **reification** time, **not** the state when the ``lazy import`` statement was evaluated. -When the module is reified, its fully qualified module name is removed from +When the module is reified, its fully qualified name is removed from ``sys.lazy_modules`` (even if there are still other unreified lazy references to it). When a package is reified and submodules in the package were also previously lazily imported, those submodules are *not* automatically reified From f2c26effca55419e53cd86a340f781e5a27c51c7 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Tue, 5 May 2026 01:06:08 +0200 Subject: [PATCH 5/6] More precision for the precision god --- peps/pep-0810.rst | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/peps/pep-0810.rst b/peps/pep-0810.rst index 87a0a0447b9..325e6b0c2e9 100644 --- a/peps/pep-0810.rst +++ b/peps/pep-0810.rst @@ -385,11 +385,11 @@ Lazy import mechanism When an import is lazy, ``__lazy_import__`` is called instead of ``__import__``. ``__lazy_import__`` has the same function signature as -``__import__``. It records the lazy import in ``sys.lazy_modules``, a -read-only mapping from each module's name to the (frozen) set of its -lazily-imported submodule names (primarily for diagnostics and -introspection), and returns a :class:`!types.LazyImportType` object for -the module. +``__import__``. It records the lazy import in an internal registry that +is exposed through ``sys.lazy_modules`` as a read-only mapping from each +module's fully qualified name to the (frozen) set of its lazily-imported +submodule names (primarily for diagnostics and introspection), and returns +a :class:`!types.LazyImportType` object for the module. The implementation of ``from ... import`` (the ``IMPORT_FROM`` bytecode implementation) checks if the module it's fetching from is a lazy module @@ -419,12 +419,13 @@ uses the state of the import system (e.g. ``sys.path``, ``sys.meta_path``, ``sys.path_hooks`` and ``__import__``) at **reification** time, **not** the state when the ``lazy import`` statement was evaluated. -When the module is reified, its fully qualified name is removed from -``sys.lazy_modules`` (even if there are still other unreified lazy references -to it). When a package is reified and submodules in the package were also -previously lazily imported, those submodules are *not* automatically reified -but they *are* added to the reified package's globals (unless the package -already assigned something else to the name of the submodule). +When the module is reified, its fully qualified name is removed from the +registry exposed via ``sys.lazy_modules`` (even if there are still other +unreified lazy references to it). When a package is reified and submodules +in the package were also previously lazily imported, those submodules are +*not* automatically reified but they *are* added to the reified package's +globals (unless the package already assigned something else to the name +of the submodule). If reification fails (e.g., due to an ``ImportError``), the lazy object is *not* reified or replaced. Subsequent uses of the lazy object will re-try From b66874445ac80878c7db535008630bee64c65900 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Tue, 5 May 2026 01:07:35 +0200 Subject: [PATCH 6/6] Fix amphibology --- peps/pep-0810.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/peps/pep-0810.rst b/peps/pep-0810.rst index 325e6b0c2e9..224c8938c85 100644 --- a/peps/pep-0810.rst +++ b/peps/pep-0810.rst @@ -421,11 +421,11 @@ state when the ``lazy import`` statement was evaluated. When the module is reified, its fully qualified name is removed from the registry exposed via ``sys.lazy_modules`` (even if there are still other -unreified lazy references to it). When a package is reified and submodules -in the package were also previously lazily imported, those submodules are -*not* automatically reified but they *are* added to the reified package's -globals (unless the package already assigned something else to the name -of the submodule). +unreified lazy references to the module). When a package is reified and +submodules in the package were also previously lazily imported, those +submodules are *not* automatically reified but they *are* added to the +reified package's globals (unless the package already assigned something +else to the name of the submodule). If reification fails (e.g., due to an ``ImportError``), the lazy object is *not* reified or replaced. Subsequent uses of the lazy object will re-try