Python 3.15 Lazy Imports: A Practical Guide for Developers
Python 3.15 lazy imports load modules only when first used. Learn PEP 810 syntax, how to cut startup time, and what to check before upgrading.
PostNest Opportunities
Author & ContributorSlow startup is one of the most common complaints about Python CLIs, serverless functions and big Django or FastAPI codebases. Most of that time often goes on importing modules your code never touches on a given run. Python 3.15 lazy imports fix this at the language level: with the new lazy keyword from PEP 810, a module is only loaded the first time you actually use it.
The timing is good. Python 3.15.0 was scheduled for 1 October 2026, but the release manager announced a surprise 3.15.0rc3 to pick up some last-minute lazy-import release blockers, with the final release about a week later. So you have a short window to try the feature on a release candidate before 3.15 lands in your CI images.
What Python 3.15 lazy imports actually do
A normal import runs the target module straight away: it finds the file, executes its top-level code and imports everything that module imports too. A lazy import binds the name to a lightweight proxy instead. The real import happens ("reifies") the first time the name is used.
lazy import json
lazy from pathlib import Path
print("Starting up...") # json and pathlib are not loaded yet
data = json.loads('{"key": "value"}') # json is imported here
After the first use the interpreter specializes the bytecode, so later accesses cost the same as a normal import. You pay for a module only if a code path needs it.
Where the lazy keyword is allowed
lazy is a soft keyword, so existing variables named lazy keep working. PEP 810 keeps the rules strict to avoid surprises:
- Module level only. Using it inside a function or class body is a
SyntaxError. - Not inside
try/except. The classic "optional dependency" pattern stays eager. - No star imports.
lazy from x import *is rejected. - No
__future__imports. These must stay eager.
Why it matters for startup time
Developers have long worked around slow imports by moving import statements inside functions. It works, but it scatters dependencies across the file, hides them from readers and linters, and has to be repeated in every function that needs the module. With lazy imports you keep a clean import block at the top of the file and still get on-demand loading.
The biggest wins are usually in:
- CLI tools where
mytool --helpshould not import pandas, boto3 or an ML framework. - Serverless and container workloads where cold-start time is billed and felt by users.
- Large monorepos and web apps with deep import graphs, where tests and management commands load far more than they use.
Don't guess at the savings; measure them. CPython's existing -X importtime flag prints a per-module import cost tree, which shows you which imports are worth making lazy:
python -X importtime -c "import mytool.cli" 2> import.log
sort -t'|' -k2 -n import.log | tail -20
Adopting lazy imports without breaking older Pythons
The lazy keyword is a syntax error on Python 3.14 and earlier, so libraries that support several versions can't use it yet. PEP 810 provides a compatible route: a module-level __lazy_modules__ list. Python 3.15 treats imports of the listed modules as potentially lazy, and older versions simply ignore the variable.
__lazy_modules__ = ["json", "pandas"]
import json # lazy on 3.15+, eager on older versions
import pandas as pd
This is the option most library authors and founders shipping SDKs should start with. You get the speed-up on new interpreters with no version checks.
Global modes and filters
You can also control laziness for the whole process:
-X lazy_imports=normal(the default) honours only explicitlazyimports.-X lazy_imports=allmakes every eligible module-level import potentially lazy.-X lazy_imports=noneforces everything eager, which is handy for debugging.
The same modes are available through the PYTHON_LAZY_IMPORTS environment variable and sys.set_lazy_imports(). For finer control, sys.set_lazy_imports_filter() takes a callable that receives the importing module, the imported name and the fromlist, and returns True to allow a lazy import or False to force an eager one. That is useful for keeping modules with import-time side effects, such as plugin registries, eager.
Gotchas to check before you ship
- Import errors move. A missing or broken module now raises
ImportErrorat first use, not at startup. The traceback is chained so it shows both the import line and the access site, but a crash can now happen mid-request instead of at boot. Keep a smoke test that touches every lazy module. - Side effects run later. If a module registers handlers, patches libraries or reads configuration when it is imported, deferring it changes the order of events. Keep those imports eager.
- Introspection sees proxies.
globals()and a module's__dict__return lazy objects without loading them. Tools that walk namespaces may need to call the proxy'sresolve()method.
Other Python 3.15 features worth knowing
Lazy imports are the headline, but the What's New in Python 3.15 page lists several other changes developers will notice:
frozendictbuilt-in (PEP 814): an immutable, hashable mapping that keeps insertion order. It works well for config objects and cache keys.- Tachyon sampling profiler (PEP 799): a new
profilingpackage with a statistical profiler that can attach to a running process by PID and export flame graphs, heatmaps or Firefox Profiler files. - UTF-8 by default (PEP 686): Python now uses UTF-8 as its default encoding regardless of system locale. You can opt out with
PYTHONUTF8=0. - Unpacking in comprehensions (PEP 798):
[*L for L in lists]flattens a list of lists in one expression.
A quick upgrade checklist
- Install the latest 3.15 release candidate in a side environment (for example with pyenv or uv) and run your test suite.
- Profile startup with
-X importtimeand list your five most expensive imports. - Make those lazy with
__lazy_modules__if you support older Pythons, or thelazykeyword if you don't. - Try
-X lazy_imports=allin staging to see what breaks, then keep side-effect modules eager with a filter. - Add a smoke test that exercises every lazily imported module so deferred
ImportErrors surface in CI rather than production.
Conclusion
Python 3.15 lazy imports turn a long-standing workaround into a clear, explicit language feature. For CLIs, serverless functions and large codebases, they are one of the cheapest performance gains you'll find in this release. Measure your imports, start with the most expensive ones, and keep side effects eager.
Tried lazy imports on your own project? Share your before-and-after startup numbers, or write up your migration, as a guest post on PostNest. It's free for developers, tech writers and startups.
