Python 3.15 lazy imports explained: how PEP 810 works

Python 3.15 lazy imports (PEP 810) explained: the lazy keyword, -X lazy_imports, __lazy_modules__, startup gains, pitfalls and how to test your code.

Python 3.15 adds explicit lazy imports through PEP 810. Put the new soft keyword lazy in front of an import, as in lazy import json or lazy from pathlib import Path, and Python binds a lightweight placeholder to the name instead of loading the module; the real import runs the first time your code uses that name. The feature is opt-in, works only at module level and is aimed at slow startup in command-line tools and large applications.

As of October 5, 2026, the final Python 3.15.0 release is scheduled for October 9, 2026, according to the PEP 790 release schedule; the third and last planned release candidate shipped on October 2 and already includes the feature.

Why Python added lazy imports

A normal import loads a module on the spot and runs its top-level code. Because most projects put imports at the top of every file, starting a program can set off a chain of imports for modules a particular run never touches; the PEP points to command-line tools that load dozens of modules just to print --help.

The usual workaround is to move imports inside functions. The PEP's authors found that about 17% of standard-library imports outside tests already sit inside functions or methods for this reason, which hides a module's dependencies. The older importlib.util.LazyLoader needs manual setup and does not handle from ... import statements.

An earlier proposal, PEP 690, would have added a switch to make a program's imports lazy across the board, and it was rejected. PEP 810 makes laziness an explicit, per-import choice. The Python Steering Council accepted it unanimously on November 3, 2025.

The syntax: lazy import and lazy from ... import

The keyword goes in front of either import form. In a script, the module stays out of sys.modules until the name is used:

lazy import json
import sys

print("json" in sys.modules) # False: not loaded yet
result = json.dumps({"hello": "world"}) # json loads here
print("json" in sys.modules) # True

A few rules from the language reference and the PEP:

  • With lazy from json import dumps, loads, each name gets its own placeholder. The first access to either name loads the whole json module but resolves only the name you touched; the other stays a placeholder until it is used.
  • Once resolved, the binding behaves like one made by a normal import.
  • Because lazy is a soft keyword, it only has meaning right before import or from. Existing code that uses lazy as a variable name keeps working.
  • Files that use the keyword fail with a SyntaxError on Python 3.14 and earlier.

Where the keyword is not allowed

Lazy imports are permitted only at module scope. Each of these raises a SyntaxError:

  • a lazy import inside a function or a class body;
  • a lazy import inside a try/except/finally block;
  • a star import, such as lazy from module import *;
  • a future statement, such as lazy from __future__ import annotations.

Opting in without the keyword

__lazy_modules__ for code that supports older versions

Libraries that still support 3.14 or older cannot use the new syntax. Instead, a module can define __lazy_modules__, a container of fully qualified module names. On 3.15 and later, regular module-level imports of those modules become lazy; on older versions the line is an ordinary assignment.

__lazy_modules__ = ["json", "pathlib"]
import json # lazy on 3.15+
import os # still eager

Relative imports are resolved to absolute names before the check, and imports inside functions, classes or try blocks stay eager regardless.

The global switch: -X lazy_imports and PYTHON_LAZY_IMPORTS

To change behavior for a whole program without editing source files, use the -X lazy_imports command-line option or the PYTHON_LAZY_IMPORTS environment variable. As of the 3.15 release candidates, both accept two values:

  • normal, the default: only imports marked lazy, or listed in __lazy_modules__, are lazy.
  • all: every module-level import becomes potentially lazy, except imports inside try blocks and star imports.

The PEP text also describes a none mode that forces every import to be eager. The 3.15 documentation lists only normal and all, and the release-candidate code rejects anything else, so a call such as sys.set_lazy_imports("none") copied from the PEP raises a ValueError.

At runtime, sys.set_lazy_imports() and sys.get_lazy_imports() set and read the mode. The documentation says library authors should generally avoid changing it, since it affects the entire application.

Filters for selective laziness

sys.set_lazy_imports_filter() installs a callable that decides, import by import, whether a potentially lazy import really stays lazy. It receives the importing module's name, the resolved name of the imported module and the fromlist (None for plain imports), and returns True to allow laziness or False to force an eager import. To make only your own package lazy:

import sys
sys.set_lazy_imports_filter(lambda importer, name, fromlist: name.startswith("myapp."))
sys.set_lazy_imports("all")

The placeholder type is exposed as types.LazyImportType; its resolve() method forces the import and returns the real object.

How much faster does startup get?

There is no fixed figure: the gain depends on how many imported modules a given run never uses. If a program needs a module straight away, a lazy import only moves the cost to a later line.

The PEP says lazy imports can cut startup time for command-line tools by 50 to 70% in practice and that memory savings of 30 to 40% have been observed in real workloads. For production data it points to case studies from Meta, which built lazy imports into its Cinder runtime, and from Hudson River Trading's Python fork, while noting that results depend on the codebase. Treat these as reported results, not a promise for your code.

Measure before and after with the long-standing -X importtime option, which prints the time spent on each import, with and without nested imports:

python -X importtime -c "import myapp"

Pitfalls to check before switching

  • Import side effects run later, or never. Modules that register plugins, patch other code or change global state when imported will not do so until first use. The PEP calls the registry pattern, where registries are filled in at import time, often by decorators, perhaps the most common case and recommends an explicit discovery function instead.
  • Errors move. A missing module or a typo in a from-import no longer fails on the import line. The ImportError appears where the name is first used, and the traceback shows both that line and the original import statement. If loading fails, the placeholder is kept and the next use tries again.
  • Import state is read at first use. Resolution uses sys.path and other import settings as they are when the name is first accessed, not when the lazy import line ran.
  • Submodules need explicit imports. If foo's own __init__ does not import bar, code that runs import foo and then uses foo.bar only works because something else imported foo.bar. The PEP recommends writing import foo.bar.
  • Introspection sees placeholders. globals() and a module's __dict__ do not trigger loading, so code that walks a namespace can meet LazyImportType objects.
  • Dynamic imports are unchanged. __import__() and importlib.import_module() stay eager.

How to test lazy imports in your project

  1. Profile startup with -X importtime and note the heaviest modules that a typical run does not need.
  2. Mark those imports lazy, or list them in __lazy_modules__ if you still support older Pythons. Imports used only in type annotations are candidates too; the PEP says lazy imports remove the need for many if TYPE_CHECKING: guards.
  3. Confirm the effect: check whether a module is in sys.modules after startup, inspect sys.lazy_modules (a debugging set that may contain extra entries), or test isinstance(globals()["json"], types.LazyImportType).
  4. Run the test suite with PYTHON_LAZY_IMPORTS=all to find code that depends on import-time side effects, then exclude those modules with a filter or keep their imports eager.
  5. Check that your linters and formatters support the 3.15 grammar; the PEP notes they need updates to parse the keyword.

Other Python 3.15 highlights

  • UTF-8 by default (PEP 686). open() and other I/O calls without an encoding argument now use UTF-8 regardless of the system locale. PYTHONUTF8=0 or -X utf8=0 restores the old behavior; passing encoding explicitly remains the safest option.
  • frozendict (PEP 814). A built-in immutable mapping that keeps insertion order, is hashable when all its keys and values are hashable, and is not a subclass of dict.
  • Tachyon profiler (PEP 799). A new profiling package adds profiling.sampling, a statistical profiler that can attach to a running process by PID and sample at up to 1,000,000 Hz. The old profile module is deprecated.
  • Upgraded JIT. The experimental JIT compiler gets a new tracing frontend. As of the release candidates, the What's New page cites a 7 to 8% geometric-mean pyperformance speedup over the standard interpreter on x86-64 Linux and 11 to 12% over the tail-calling interpreter on AArch64 macOS.

The full list, including a sentinel built-in and unpacking in comprehensions, is in What's New in Python 3.15. Under PEP 790, 3.15 gets bugfixes for about two years and security fixes until about October 2031. More coverage of languages and tools is in our Developers and Open Source sections, and other reference pages are collected under Guides.

Frequently asked questions

What is a lazy import in Python?

A lazy import binds a name to a placeholder and delays loading the module until that name is first used. In Python 3.15 you request one with the lazy keyword, for example lazy import json.

Which Python version supports lazy imports?

The lazy keyword arrives in Python 3.15; the final release is scheduled for October 9, 2026, and the 3.15 release candidates already support it. On 3.14 and earlier the syntax is an error, so code that must run on both should list modules in __lazy_modules__ instead.

How do I make all imports lazy in Python 3.15?

Run Python with -X lazy_imports=all or set the environment variable PYTHON_LAZY_IMPORTS=all. Module-level imports then become potentially lazy, except those inside try blocks and star imports.

Is PEP 810 the same as importlib.util.LazyLoader?

No. LazyLoader is an older import-machinery tool that needs manual setup and does not support from ... import statements, while PEP 810 adds language syntax that tools can recognize.

Do lazy imports make Python code run faster?

They mainly reduce startup time and memory when some imported modules go unused in a run. Once a module loads, it behaves like a normal import.