Python Lazy Imports (PEP 810): How They Work + Examples

Python 3.15 lazy imports (PEP 810) explained: the lazy keyword, how it works, code examples, startup and memory gains, pros and cons, and when to use it.

By Danyal Ahmed - Data Scientist · · 8 min read

Python 3.15 adds explicit lazy imports through PEP 810. You write lazy import json instead of import json, and Python binds the name to a lightweight placeholder right away but only loads the module the first time you actually use it. That moves import cost from startup to the moment of first use, which can make command-line tools, serverless functions and large applications start faster and use less memory.

This guide explains the syntax, how lazy imports work under the hood, the code patterns you will use, the real pros and cons, the restrictions, and when you should and should not reach for them. Facts here come from PEP 810 and the official Python 3.15 documentation, which states that Python 3.15 was released on 9 October 2026.

Key takeaways

  • Syntax: lazy import module and lazy from module import name. lazy is a soft keyword that only applies to import statements.
  • Behaviour: the module loads on first use of the name, not when the import line runs.
  • Opt-in and local. Ordinary imports keep working exactly as before.
  • Errors move later. A missing module raises its error at first use, not at startup.
  • Module scope only. lazy inside a function, class or try block is a SyntaxError, and so are star imports.
  • Best for: startup-sensitive programs with heavy or rarely used dependencies.

What is a lazy import?

A normal import does real work when the line executes: it finds the module, runs its top-level code and caches the result. If your program imports many large libraries at the top, you pay for all of them before the first useful line runs, even if some code paths never use them.

A lazy import defers that work. The name exists immediately, but it points to a proxy. The first time you use the name, Python performs the actual import and swaps the proxy for the real object.

Lazy import syntax

pythonlazy import json
lazy from pathlib import Path

print("Starting up...")  # json and pathlib are not fully imported yet

data = json.loads('{"key": "value"}')  # json gets fully imported here
p = Path(".")                          # pathlib gets fully imported here

lazy is a soft keyword, so it does not become a reserved word everywhere and existing code that uses lazy as a variable name keeps working. It is only recognised in front of an import statement.

How lazy imports work

  1. Bind a proxy. When lazy import json runs, Python creates a lazy proxy and binds the name json to it immediately.
  2. Wait for first use. Nothing is loaded until you access the name. The proxy type is available as types.LazyImportType.
  3. Reify on access. The first access calls __import__ at that moment, loads the module and replaces the proxy with the real object. This replacement is called reification.
  4. Run at full speed afterwards. After reification, the interpreter's adaptive specialisation removes the lazy check from the access path, typically after two or three accesses, according to the PEP.

lazy from ... import

With lazy from module import name, the first access to any imported name loads the whole module but reifies only that name. The other names from the same statement stay lazy until you use them.

What does not become lazy

Laziness is not recursive. If you lazily import module A, then when A finally loads, its own normal imports run normally. Only the statements you mark lazy are deferred.

Code examples

Example 1: a CLI that starts fast

python# cli.py
import argparse

lazy import pandas
lazy import sklearn

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--report", action="store_true")
    args = parser.parse_args()

    if args.report:
        df = pandas.read_csv("data.csv")   # pandas loads here, only now
        print(df.describe())

main()

Running python cli.py --help or any path that never touches pandas skips that import cost entirely. sklearn is never loaded here because nothing uses it.

Example 2: errors appear at first use

pythonlazy import not_a_real_module   # no error here

print("app started")            # still no error

not_a_real_module.run()         # ModuleNotFoundError is raised here

The traceback shows both the line where you accessed the name and the original lazy import line, so you can trace the cause. If a reification fails, the proxy stays in place and the next access tries again.

Example 3: supporting older Python versions

The lazy keyword is only understood by Python 3.15 and later, so using it in code that must run on older versions would fail. For that case, PEP 810 provides a module-level list:

python__lazy_modules__ = ["json", "pathlib"]

import json     # lazy on 3.15+, eager on older Python
import os       # still eager

On Python 3.15 and later, regular imports of modules named in __lazy_modules__ behave as if marked lazy. Earlier versions ignore the variable and import eagerly, so the same file works on both.

Example 4: turning lazy imports on globally

You can change behaviour without editing code:

bashpython -X lazy_imports=all app.py
PYTHON_LAZY_IMPORTS=all python app.py

Or inside Python:

pythonimport sys
sys.set_lazy_imports("all")

The documented modes include normal (the default, which honours the lazy keyword) and all (every eligible module-level import is potentially lazy, except those in try blocks and star imports). The PEP also describes a none mode that forces eager imports. If settings conflict, sys.set_lazy_imports() takes precedence over -X, which takes precedence over the environment variable.

Example 5: a filter to be selective

pythonimport sys

def myapp_filter(importing, imported, fromlist):
    return imported.startswith("myapp.")

sys.set_lazy_imports_filter(myapp_filter)
sys.set_lazy_imports("all")

import myapp.slow_module  # lazy (matches filter)
import json               # eager (does not match filter)

The filter receives the importing module, the imported module name and the fromlist, and returns True to allow laziness or False to force an eager import. It runs when the lazy statement executes, not at reification.

Restrictions

Code Result
lazy import x inside a function SyntaxError
lazy import x inside a class body SyntaxError
lazy import x inside try / except / finally SyntaxError
lazy from module import * SyntaxError
lazy from __future__ import ... SyntaxError

Imports inside functions are already deferred until the function runs, so you do not need lazy there.

Pros and cons

Pros Cons
Faster startup when many imports go unused Import errors surface late, at first use
Lower memory when heavy modules never load Import-time side effects run late or never
Explicit and local, so you choose each import Circular imports are not automatically fixed
Backward compatible with normal imports Only module scope, no lazy in functions or try
Cleaner than hand-written deferred import hacks Linters, formatters and IDEs must learn the new syntax
Can break some import cycles A slower first call when the deferred import finally runs
Works with from imports, unlike older approaches Behaviour differs between 3.15 and earlier versions

How big are the gains?

The PEP's motivation section cites startup-time reductions of 50 to 70% and memory savings of 30 to 40% in real workloads, and a 10 to 20% improvement in a PySide example. Those are reports from the PEP's authors for specific programs, so treat them as examples, not guarantees. The PEP's own benchmark of 278 standard-library modules, forced to load, shows lazy imports add roughly 0.1 to 0.5% overhead when everything is eventually needed, so the cost of using the feature is small. Measure your own app before and after.

Common pitfalls

  • Registration side effects. If importing a module registers a plugin, route or decorator as a side effect, deferring it means the registration may not happen when you expect. The PEP recommends explicit discovery functions instead.
  • Submodules. Import submodules explicitly. Do not rely on them appearing as attributes of the parent.
  • Fail-fast checks. A service that should crash at boot when a dependency is missing may not do so if that dependency is lazy. Keep critical imports eager.
  • Dynamic package installs. Installing packages at run time can change what a lazy name resolves to. Tools that do this should reify first or disable lazy mode.
  • Circular imports. They still fail if the cycle is touched during import. Lazy imports can help break some cycles but are not a guaranteed fix. See clean architecture explained for structuring dependencies.
  • Inspecting namespaces. globals() and a module's __dict__ return the raw namespace without triggering loading, so you may see proxies. dir() is special-cased not to trigger loading, while attribute access and getattr() do.

When to use lazy imports

Good fits:

  • Command-line tools where many commands, or --help, never need heavy libraries.
  • Serverless functions and short jobs where cold-start time matters. See serverless explained.
  • Large applications with many optional, rarely used dependencies.
  • Test suites and developer tooling that import a lot but run little.
  • Data and AI scripts that import big libraries such as pandas or machine learning frameworks on only some paths. See Python pandas data cleaning.

When not to use them

  • Modules with important import-time side effects such as plugin or registry decorators.
  • Services that must fail fast at startup when a dependency is missing.
  • Long-running servers where a one-time startup cost is irrelevant, and a surprise delay on the first request is worse. You can warm up by touching the name at boot.
  • Code that must run on Python versions older than 3.15 unless you use __lazy_modules__.
  • Imports that are already needed immediately. Making them lazy only adds indirection.

Lazy imports vs other approaches

Approach How Limits
Normal import Loads at the import line Pays full cost up front
Import inside a function Loads when the function first runs Verbose, hides dependencies, repeated lookups
importlib.util.LazyLoader Library-level lazy loading No from ... import support, resolves the module spec up front, needs boilerplate and has no syntax that tools can recognise
PEP 810 lazy import Language syntax Python 3.15 and later, module scope only

PEP 810 replaces an earlier, broader proposal (PEP 690) that was rejected in favour of this explicit, local, opt-in design.

How to adopt lazy imports safely

  1. Measure first. Run python -X importtime your_app.py to see which imports cost the most.
  2. Upgrade to Python 3.15 in a test environment.
  3. Mark the heaviest imports lazy, one at a time, and re-measure.
  4. Run your tests and check anything that relies on import side effects.
  5. Keep critical, fail-fast imports eager.
  6. Check your tools. Make sure your linter, formatter, type checker and IDE support the lazy keyword, since they need updates to understand it.
  7. Use the global mode carefully. -X lazy_imports=all is a quick experiment, while production use is safer with explicit lazy markers or a filter.

For backend context, see the backend developer roadmap and how fast is the CPU, cache, RAM and SSD for why startup work costs time.

Need Python performance work?

We build and tune Python backends, data pipelines and AI services, including startup time, memory and deployment. Get a scope estimate for your Python project, or see how we deploy FastAPI apps on a VPS.

FAQ

What are lazy imports in Python?

Lazy imports defer loading a module until you first use it. In Python 3.15, you mark an import with the lazy keyword, such as lazy import json, and Python loads the module only when the name is first accessed.

Which Python version has lazy imports?

Python 3.15, through PEP 810. The official 3.15 documentation says Python 3.15 was released on 9 October 2026.

How do I write a lazy import?

Put lazy before the import: lazy import json or lazy from pathlib import Path. The statement must be at module level.

Are lazy imports faster?

They can make startup faster and reduce memory by skipping imports you never use. The PEP cites 50 to 70% startup reductions and 30 to 40% memory savings in real workloads, but results depend on your program, so measure.

Can I use lazy inside a function?

No. lazy inside a function, class body or try block raises a SyntaxError. Imports inside functions are already deferred until the function runs.

What happens if a lazy import fails?

The error is raised at the first use of the name, not when the import line runs. The traceback shows both the access and the original import, and the next access tries again.

Do lazy imports work on older Python versions?

The lazy keyword does not. For code that must support older versions, list module names in __lazy_modules__. Python 3.15 and later treat those imports as lazy, and older versions ignore the list and import normally.

Should I make every import lazy?

No. Keep imports eager when they have side effects, when you want missing dependencies to fail at startup, or when they are needed right away. Use lazy imports for heavy, rarely used dependencies.

Sources