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.
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 moduleandlazy from module import name.lazyis 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.
lazyinside a function, class ortryblock is aSyntaxError, 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
- Bind a proxy. When
lazy import jsonruns, Python creates a lazy proxy and binds the namejsonto it immediately. - Wait for first use. Nothing is loaded until you access the name. The proxy type is available as
types.LazyImportType. - 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. - 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 andgetattr()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
- Measure first. Run
python -X importtime your_app.pyto see which imports cost the most. - Upgrade to Python 3.15 in a test environment.
- Mark the heaviest imports
lazy, one at a time, and re-measure. - Run your tests and check anything that relies on import side effects.
- Keep critical, fail-fast imports eager.
- Check your tools. Make sure your linter, formatter, type checker and IDE support the
lazykeyword, since they need updates to understand it. - Use the global mode carefully.
-X lazy_imports=allis a quick experiment, while production use is safer with explicitlazymarkers 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.