NEW in Python 3.15: Explicit Lazy Imports
Key Points
- 1PEP 810 introduces explicit lazy imports in Python 3.15+, allowing modules to be loaded only when their imported names are first used, significantly reducing startup time and memory for applications with many dependencies.
- 2The `lazy` keyword (e.g., `lazy import json`) creates an immediate binding, but the actual module or name is loaded and reified upon first access, after which it behaves identically to a normal import, though import errors are deferred until this point.
- 3Primarily benefiting command-line tools and type annotations, lazy imports are restricted to the module level, and for backward compatibility, existing `import` statements can be made lazy on Python 3.15+ by listing the module in the `lazy_modules` set.
PEP 810 introduces explicit lazy imports in Python 3.15, deferring module loading until the first time an imported name is actually used. This mechanism aims to reduce startup time and memory consumption for applications, particularly command-line tools that may only use a subset of their dependencies in any given execution path.
The syntax for lazy imports is straightforward: or . In both cases, a binding is created immediately, but the actual module or specific name is not loaded into memory until its first access. After this initial use, the import behaves identically to a normal, eager import, incurring no ongoing overhead.
For , each imported name acts as a lazy proxy. Upon the first access of any of these names, the entire module is loaded, and then only the specifically accessed name is reified (converted from its lazy proxy to the actual object).
Reification Process:
When a lazy name is first used, Python's standard import machinery (including import hooks and loaders) is executed at that moment. The state of the import system, including sys.path and sys.meta_path, is used at the time of reification, not at the time the lazy import statement was written. Post-reification, the binding is indistinguishable from a normal import, and the interpreter can even optimize the bytecode to ensure zero overhead on subsequent accesses.
Error Handling:
A key difference with lazy imports is the timing of ImportError exceptions. While a normal import raises an ImportError at the import line, a lazy import defers this error until the first use of the imported name. Tracebacks for such errors are designed to be helpful, showing both where the lazy import was defined and where the error occurred due to its first use.
Restrictions:
Lazy imports have specific contextual limitations:
- They are only permitted at the module level. They cannot be used inside functions, class bodies, or
tryblocks. - Star imports () are not allowed.
from __future__ importstatements cannot be lazy.
try-except blocks if the intention is to catch ImportError at the point of use, as the error is deferred.Introspection and Forced Reification:
Accessing globals() or a module's standard dictionary (module.__dict__) does not trigger reification, preventing accidental eager loading during introspection. To force reification of a lazy object without using it, one can call its .resolve() method. The dir() function, when applied at the module level, is special-cased to avoid triggering reification, allowing inspection of lazy state without side effects.
Use Cases and Considerations:
The primary benefits of lazy imports are reduced startup time and memory footprint, especially for command-line tools or applications with many subcommands. They are also well-suited for type annotation imports (e.g., lazy from typing import ...), potentially replacing if typing.TYPE_CHECKING: blocks by avoiding runtime cost until types are actually used.
A critical consideration is handling import-time side effects. If a module performs actions upon import (e.g., registering plugins or applying decorators), lazy importing means these actions occur at the time of first use, potentially altering the order of registration or execution compared to eager imports. The PEP suggests explicit discovery mechanisms (e.g., a dedicated function to import and register) as a solution.
Backward Compatibility with lazy_modules:
For codebases requiring compatibility with Python versions prior to 3.15 while leveraging lazy imports on newer versions, PEP 810 introduces the concept of lazy_modules. This is a module-level set or list containing fully qualified module names. On Python 3.15 and above, any import X or from X import Y statement where X is present in lazy_modules will be treated as if it were a lazy import X or lazy from X import Y. On older Python versions, lazy_modules is ignored, and all imports remain eager. This allows for a single codebase that automatically benefits from lazy loading on compatible Python versions.
The recommendation is to use explicit lazy keyword where imports would traditionally be moved into functions to optimize startup, keeping dependencies visible at the module level. For cross-version support, lazy_modules is the preferred approach. The feature, while described in detail, was in an alpha state for Python 3.15 at the time of the video's creation, meaning syntax and final details were subject to change and not yet runnable without syntax errors.