Skip to content

Commit 861f17f

Browse files
authored
PEP 810: Add lazy objects to the spec. (#4643)
Add lazy objects to the spec, clarify what happens when reification fails, tweak some of the language.
1 parent d8a9c78 commit 861f17f

File tree

1 file changed

+60
-44
lines changed

1 file changed

+60
-44
lines changed

peps/pep-0810.rst

Lines changed: 60 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -335,19 +335,29 @@ An import remains lazy only if the filter function returns ``True``.
335335

336336
If no lazy import filter is set, all *potentially lazy* imports are lazy.
337337

338+
Lazy objects
339+
------------
340+
341+
Lazy modules, as well as names lazy imported from modules, are represented
342+
by :class:`!types.LazyImportType` instances, which are resolved to the real
343+
object (reified) before they can be used. This reification is usually done
344+
automatically (see below), but can also be done by calling the lazy object's
345+
``get`` method.
346+
338347
Lazy import mechanism
339348
---------------------
340349

341350
When an import is lazy, ``__lazy_import__`` is called instead of
342351
``__import__``. ``__lazy_import__`` has the same function signature as
343352
``__import__``. It adds the module name to ``sys.lazy_modules``, a set of
344353
fully-qualified module names which have been lazily imported at some point
345-
(primarily for diagnostics and introspection), and returns a "lazy module
346-
object."
354+
(primarily for diagnostics and introspection), and returns a
355+
:class:`!types.LazyImportType`` object for the module.
347356

348357
The implementation of ``from ... import`` (the ``IMPORT_FROM`` bytecode
349358
implementation) checks if the module it's fetching from is a lazy module
350-
object, and if so, returns a lazy object for each name instead.
359+
object, and if so, returns a :class:`!types.LazyImportType` for each name
360+
instead.
351361

352362
The end result of this process is that lazy imports (regardless of how they
353363
are enabled) result in lazy objects being assigned to global variables.
@@ -356,34 +366,36 @@ Lazy module objects do not appear in ``sys.modules``, they're just listed in
356366
the ``sys.lazy_modules`` set. Under normal operation lazy objects should only
357367
end up stored in global variables, and the common ways to access those
358368
variables (regular variable access, module attributes) will resolve lazy
359-
imports ("reify") and replace them when they're accessed.
369+
imports (reify) and replace them when they're accessed.
360370

361371
It is still possible to expose lazy objects through other means, like
362372
debuggers. This is not considered a problem.
363373

364374
Reification
365375
-----------
366376

367-
When a lazy object is first used, it needs to be reified. This means resolving
368-
the import at that point in the program and replacing the lazy object with the
369-
concrete one. Reification imports the module in the same way as it would have
370-
been if it had been imported eagerly. Notably, reification still calls
371-
``__import__`` to resolve the import, which uses the state of the import system
372-
(e.g. ``sys.path``, ``sys.meta_path``, ``sys.path_hooks`` and ``__import__``)
373-
at **reification** time, **not** the state when the ``lazy import`` statement
374-
was evaluated.
375-
376-
When the module is first reified, it's removed from ``sys.lazy_modules`` (even
377-
if there are still other unreified lazy references to it). When a package is
377+
When a lazy object is used, it needs to be reified. This means resolving the
378+
import at that point in the program and replacing the lazy object with the
379+
concrete one. Reification imports the module at that point in the program.
380+
Notably, reification still calls ``__import__`` to resolve the import, which
381+
uses the state of the import system (e.g. ``sys.path``, ``sys.meta_path``,
382+
``sys.path_hooks`` and ``__import__``) at **reification** time, **not** the
383+
state when the ``lazy import`` statement was evaluated.
384+
385+
When the module is reified, it's removed from ``sys.lazy_modules`` (even if
386+
there are still other unreified lazy references to it). When a package is
378387
reified and submodules in the package were also previously lazily imported,
379388
those submodules are *not* automatically reified but they *are* added to the
380-
reified package's globals (unless the package already assigned something else
381-
to the name of the submodule).
389+
reified package's globals (unless the package already assigned something
390+
else to the name of the submodule).
382391

383-
If reification fails (e.g., due to an ``ImportError``), the exception is
384-
enhanced with chaining to show both where the lazy import was defined and
385-
where it was first accessed (even though it propagates from the code that
386-
triggered reification). This provides clear debugging information:
392+
If reification fails (e.g., due to an ``ImportError``), the lazy object is
393+
*not* reified or replaced. Subsequent uses of the lazy object will re-try
394+
the reification. Exceptions that happen during reification are raised as
395+
normal, but the exception is enhanced with chaining to show both where the
396+
lazy import was defined and where it was accessed (even though it propagates
397+
from the code that triggered reification). This provides clear debugging
398+
information:
387399

388400
.. code-block:: python
389401
@@ -469,7 +481,7 @@ Example using ``globals()``:
469481
g = globals()
470482
471483
print('json' in sys.modules) # False - still lazy
472-
print(type(g['json'])) # <class 'lazy_import'>
484+
print(type(g['json'])) # <class 'LazyImport'>
473485
474486
# Explicitly reify using the get() method
475487
resolved = g['json'].get()
@@ -704,7 +716,7 @@ These changes are limited to bindings explicitly made lazy:
704716

705717
* **Error timing.** Exceptions that would have occurred during an eager import
706718
(for example ``ImportError`` or ``AttributeError`` for a missing member) now
707-
occur at the first *use* of the lazy name.
719+
occur at the *use* of the lazy name.
708720

709721
.. code-block:: python
710722
@@ -714,7 +726,7 @@ These changes are limited to bindings explicitly made lazy:
714726
# With lazy import - error deferred
715727
lazy import broken_module
716728
print("Import succeeded")
717-
broken_module.foo() # ImportError raised here on first use
729+
broken_module.foo() # ImportError raised here on use
718730
719731
* **Side-effect timing.** Import-time side effects in lazily imported modules
720732
occur at first use of the binding, not at module import time.
@@ -727,16 +739,17 @@ These changes are limited to bindings explicitly made lazy:
727739
when it is first used.
728740
* **Proxy visibility.** Before first use, the bound name refers to a lazy
729741
proxy. Indirect introspection that touches the value may observe a proxy
730-
lazy object representation. After first use, the name is rebound to the real
731-
object and becomes indistinguishable from an eager import.
742+
lazy object representation. After first use (provied the module was
743+
imported succesfully), the name is rebound to the real object and becomes
744+
indistinguishable from an eager import.
732745

733746
Thread-safety and reification
734747
-----------------------------
735748

736-
First use of a lazy binding follows the existing import-lock discipline.
737-
Exactly one thread performs the import and **atomically rebinds** the
738-
importing module's global to the resolved object. Concurrent readers
739-
thereafter observe the real object.
749+
Reification follows the existing import-lock discipline. Exactly one thread
750+
performs the import and **atomically rebinds** the importing module's global
751+
to the resolved object. Concurrent readers thereafter observe the real
752+
object.
740753

741754
Lazy imports are thread-safe and have no special considerations for
742755
free-threading. A module that would normally be imported in the main thread
@@ -758,11 +771,11 @@ code that doesn't.
758771
Runtime performance
759772
~~~~~~~~~~~~~~~~~~~
760773

761-
After reification (first use), lazy imports have **zero overhead**. The
762-
adaptive interpreter specializes the bytecode (typically after 2-3 accesses),
763-
eliminating any checks. For example, ``LOAD_GLOBAL`` becomes
764-
``LOAD_GLOBAL_MODULE``, which directly accesses the module identically to
765-
normal imports.
774+
After reification (provided the import was succesful), lazy imports have
775+
**zero overhead**. The adaptive interpreter specializes the bytecode
776+
(typically after 2-3 accesses), eliminating any checks. For example,
777+
``LOAD_GLOBAL`` becomes ``LOAD_GLOBAL_MODULE``, which directly accesses the
778+
module identically to normal imports.
766779

767780
The `pyperformance suite`_ confirms the implementation is performance-neutral.
768781

@@ -1033,6 +1046,9 @@ it was first used:
10331046
1/0
10341047
ZeroDivisionError: division by zero
10351048
1049+
Exceptions during reification prevent the replacement of the lazy object,
1050+
and subsequent uses of the lazy object will retry the whole reification.
1051+
10361052
How do lazy imports affect modules with import-time side effects?
10371053
-----------------------------------------------------------------
10381054

@@ -1118,8 +1134,8 @@ What's the performance overhead of lazy imports?
11181134

11191135
The overhead is minimal:
11201136

1121-
- Zero overhead after first use thanks to the adaptive interpreter optimizing
1122-
the slow path away.
1137+
- Zero overhead after first use (provided the import doesn't fail) thanks to
1138+
the adaptive interpreter optimizing the slow path away.
11231139
- Small one-time cost to create the proxy object.
11241140
- Reification (first use) has the same cost as a regular import.
11251141
- No ongoing performance penalty.
@@ -1159,7 +1175,7 @@ error. If lazy imports are globally enabled, star imports will still be eager.
11591175
How do lazy imports interact with import hooks and custom loaders?
11601176
------------------------------------------------------------------
11611177

1162-
Import hooks and loaders work normally. When a lazy object is first used,
1178+
Import hooks and loaders work normally. When a lazy object is used,
11631179
the standard import protocol runs, including any custom hooks or loaders that
11641180
were in place at reification time.
11651181

@@ -1191,7 +1207,7 @@ reifies all lazy imports in that module first. This design ensures:
11911207
lazy import json
11921208
11931209
g = globals()
1194-
print(type(g['json'])) # <class 'lazy_import'> - your problem
1210+
print(type(g['json'])) # <class 'LazyImport'> - your problem
11951211
11961212
# From external code:
11971213
import sys
@@ -1390,11 +1406,11 @@ The best practice is still to avoid circular imports in your code design.
13901406
Will lazy imports affect the performance of my hot paths?
13911407
---------------------------------------------------------
13921408

1393-
After first use, lazy imports have **zero overhead** thanks to the adaptive
1394-
interpreter. The interpreter specializes the bytecode (e.g., ``LOAD_GLOBAL``
1395-
becomes ``LOAD_GLOBAL_MODULE``) which eliminates the lazy check on subsequent
1396-
accesses. This means once a lazy import is reified, accessing it is just as
1397-
fast as a normal import.
1409+
After first use (provided the import succeed), lazy imports have **zero
1410+
overhead** thanks to the adaptive interpreter. The interpreter specializes
1411+
the bytecode (e.g., ``LOAD_GLOBAL`` becomes ``LOAD_GLOBAL_MODULE``) which
1412+
eliminates the lazy check on subsequent accesses. This means once a lazy
1413+
import is reified, accessing it is just as fast as a normal import.
13981414

13991415
.. code-block:: python
14001416

0 commit comments

Comments
 (0)