Skip to content

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 --help dropping 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 Path

The 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 try block. lazy only 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 annotations does 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 variable
import sys
sys.set_lazy_imports("all")                     # programmatic, before further imports run

A 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 ImportError or a SyntaxError that used to fire at the import line 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-loader is 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 demandimport rewrites the import system at startup to make all imports lazy. It predates lazy-loader by years and is the canonical example of how Mercurial keeps hg startup 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

Last updated on