What is PEP 810?
PEP 810: Explicit Lazy Imports adds a lazy keyword in front of an import or from statement that defers loading the module until the imported name is first used. Authored by Pablo Galindo Salgado, Germán Méndez Bravo, Thomas Wouters, and others, the PEP was accepted unanimously by the Steering Council on 3 November 2025 for Python 3.15. Python 3.15 reached beta 1 on 7 May 2026, the first build to ship the feature. PEP 810 is the second attempt at lazy imports in CPython after the implicit-everywhere PEP 690 was withdrawn in 2023; the explicit, opt-in design is the trade that got 810 across the line.
Decide whether your program will benefit
Lazy imports speed up startup, not steady-state execution. Whether that matters depends on the shape of the program.
- Short-running CLIs and one-shot scripts win the most. A tool that does a small amount of work and exits pays the import cost on every invocation. Hugo van Kemenade measured
pypistats --helpdropping from 104 ms to 36 ms with lazy imports enabled, about 2.9x faster, in line with the 50-70% range PEP 810 cites for CLI startup. - Long-running servers and workers see no startup win after the first request. A web service that runs for hours pays its import cost once, at boot, and amortizes it over millions of requests. Lazy imports in a long-lived process change when the cost is paid (boot vs first request) but not the steady-state throughput.
- REPLs and notebooks see no win. Imports happen interactively, one at a time, in response to a human. The startup time the user notices is the interpreter’s own boot, not the imports they will type later.
The heuristic: if your program runs for a fraction of a second, lazy imports earn their cost. If it runs for hours, they probably do not.
Read the syntax
A lazy prefix marks a single import as deferred. The two shapes that work:
lazy import json
lazy from pathlib import PathThe keyword is soft: the parser only treats lazy as a keyword in front of import or from. A variable named lazy = 1 keeps working unchanged.
Several positions are explicitly forbidden by the PEP:
- Inside a function body, class body, or
tryblock.lazyonly applies to module-level imports, where the parser can statically verify it. - Star imports.
lazy from foo import *does not compile, because the import would have to load the module to find the names anyway. __future__imports.lazy from __future__ import annotationsdoes not compile, because__future__flags must be processed before module execution begins.
When code first reads Path after a lazy from pathlib import Path, the interpreter resolves the deferred reference, runs pathlib’s top-level code, binds Path to the resolved class, and continues. The import happens once; later reads pay no extra cost.
Choose the runtime mode
The lazy keyword is the per-line declaration. A separate runtime mode controls how the interpreter treats imports as a whole:
| Mode | What it does |
|---|---|
normal (default) |
Only imports prefixed with lazy defer. Every other import is eager, exactly as on Python 3.14 and earlier. |
all |
Every import is treated as if it were prefixed with lazy, except for ones the program has not opted out of. Useful for measuring the upper bound on startup wins, less useful for production. |
none |
Force every import eager, ignoring lazy keywords. Useful for debugging an issue that may be caused by deferred loading. |
The mode is set in three ways, listed from most to least specific:
python -X lazy_imports=all script.py # command line
PYTHON_LAZY_IMPORTS=all python script.py # environment variableimport sys
sys.set_lazy_imports("all") # programmatic, before further imports runA library that wants to opt its own imports into lazy behavior on Python 3.15 without changing its syntax (which would not parse on 3.14) can declare a __lazy_modules__ list at module top level. Imports that name a module in that list defer on 3.15+ and run eagerly on older Pythons. PEP 810 documents this as the migration shim for libraries with a wide supported-Python range.
Plan for what breaks
Lazy imports change when a module’s top-level code runs. Anything that depends on top-level code running at the import line, rather than at first use, can break.
- Decorator-based plugin registration. A plugin module that registers itself with a central registry at import time only registers when its names are first read. A discovery loop that imports every plugin module and then queries the registry will find nothing.
- Module-level monkey-patches. Code that patches a third-party library at import time (a common pattern in test setup and instrumentation) defers until first use. If the patch is supposed to take effect before any code in the patched library runs, lazy imports break that ordering.
- Errors move from import-time to first-use. An
ImportErroror aSyntaxErrorthat used to fire at theimportline now fires at the first attribute access. Stack traces point at the use site rather than the import line, which can confuse debugging. - Circular imports are not auto-fixed. If two modules circularly depend on each other and the cycle is exercised during initialization, marking the imports lazy does not help. It can help if the cycle is only exercised inside functions called later.
The fix in each case is the same: move the side effect into an explicit function and call it from the consumer, instead of relying on the import line as a coincidental trigger.
Compare to lazy-loader and demandimport
Third-party tools have offered lazy imports for years. The two most established:
lazy-loaderis a small library used by NumPy, SciPy, and scikit-image to defer submodule imports through module-level__getattr__plumbing. It runs on Python 3.7+ and ships in a wheel; the cost is that every package using it carries its own boilerplate and the IDE/static-analysis story is bespoke.- Mercurial’s
demandimportrewrites the import system at startup to make all imports lazy. It predateslazy-loaderby years and is the canonical example of how Mercurial keepshgstartup fast despite its size.
PEP 810 is narrower than demandimport (no implicit global lazy mode by default; the all runtime flag has to be opted into) and broader than lazy-loader (parser-level support, no per-module plumbing). The trade for 3.15+ is that the keyword is standard and IDEs, type checkers, and linters can reason about it directly. The trade against the third-party tools is the Python version floor: lazy-loader covers libraries that still support 3.7+, while lazy is 3.15-only without the __lazy_modules__ shim.
Learn More
- PEP 810 specification is the authoritative document.
- Steering Council acceptance ruling records the unanimous 3 November 2025 vote and the council’s notes on the keyword choice.
- Three times faster with lazy imports by Hugo van Kemenade walks through measured wins on a real CLI.
- Python steering council accepts lazy imports on LWN summarizes the acceptance and design trade-offs.
- What’s New in Python 3.15: lazy imports covers the feature in the broader 3.15 release notes.
- PEP 690 is the withdrawn implicit-everywhere proposal that PEP 810 supersedes.