How to Use Python 3.15 Lazy Imports Safely
Python 3.15 introduces explicit lazy imports for deferring optional dependencies. Learn how the new syntax works, how to measure its effect, and where it can cause problems.
On this page
Python 3.15.0rc2 arrived on September 1 as the final planned release candidate before the October 1 final release. For developers, one of its most practical new features is explicit lazy imports, which let you keep imports at module level while delaying the actual module load until the imported name is used.
This tutorial shows how to test Python 3.15 lazy imports in a small project, measure when a dependency is loaded, and decide where the feature is safe to use. The release candidate is still a preview, so use it for development and testing rather than production.
Start with Python 3.15 before changing your project
Python 3.15.0rc2 is the second release candidate and the final planned release candidate for the 3.15 series. The release contains roughly 144 bug fixes, build improvements, and documentation changes since the first release candidate. The Python team says there should be no further application binary interface changes in the 3.15 series, but the final release is scheduled for October 1, 2026.
Create a separate virtual environment so your existing projects continue using their normal Python version. After installing Python 3.15.0rc2, verify the interpreter with:
python --versionYou should see a version beginning with Python 3.15.0rc2. If your system has multiple Python installations, use the command appropriate to your environment, such as python3.15, instead of changing the default interpreter for every project.
See the difference between eager and lazy imports
A normal Python import is eager. When the import statement executes, Python loads the module and runs its module-level code. That is useful when the program needs the dependency immediately, but it can add startup work when a command uses only a small part of a large application.
Python 3.15 adds the lazy soft keyword for this specific situation. The following import does not load the JSON module immediately:
lazy import jsonThe module is loaded when the program first needs the json name. The important distinction is that laziness is explicit. Existing statements such as import json keep their normal behavior unless you deliberately opt into one of the new lazy-import mechanisms.
Build a small test that proves when loading happens
The easiest way to understand the feature is to watch sys.modules. This dictionary contains modules that Python has loaded into the current interpreter. Create a file called lazy_demo.py with this code:
import sys
lazy import json
print("Before use:", "json" in sys.modules)
data = {"name": "Python", "version": 3.15}
encoded = json.dumps(data)
print("After use:", "json" in sys.modules)
print(encoded)Run the file with Python 3.15. Before json is used, the first check should report False. Calling json.dumps() then triggers the deferred import, so the second check should report True. That gives you a simple way to verify that the import is actually being delayed rather than merely assuming it is.
Use lazy imports where startup work is optional
The feature is most useful when a program has dependencies that are not required on every execution path. A command-line application is a good example. Imagine a tool with commands for displaying configuration, generating reports, and exporting data. A large reporting library might be unnecessary when the user only asks for configuration information.
Instead of hiding the import inside a function, Python 3.15 lets you keep the dependency visible at module level:
lazy import reporting
def export_report(records):
return reporting.create_report(records)This preserves a clear dependency declaration while postponing the actual loading work. Once reporting is used, Python resolves the lazy binding and subsequent accesses behave like an ordinary imported module.
Lazy imports also work with selected names
You can defer selected names with a from import. For example:
lazy from json import dumps, loads
print(dumps({"status": "ready"}))The imported names are represented by lazy objects until they are accessed. Using dumps causes the JSON module to load, after which the name resolves to the actual function. This can be useful when an application imports only a few functions from a module but does not need them during startup.
There is one important restriction: wildcard imports cannot be lazy. A statement such as lazy from package import * is not supported because Python has to inspect the module to determine which names the wildcard should expose.
Do not make every import lazy
Lazy imports are not automatically a performance win. They move work from startup to the first operation that needs the dependency. If the program always uses a module immediately after startup, delaying its import may simply move the same cost to a later point.
There is another issue: import-time side effects. Some packages perform registration, configuration, plugin discovery, or other setup when their modules are imported. With an eager import, that work happens immediately. With a lazy import, it waits until the imported name is accessed. Code that depends on registration having already happened can therefore behave differently.
A good first target is a dependency that is both expensive to load and genuinely optional for common execution paths. Do not convert the entire import section simply because the syntax is available.
Keep compatibility with older Python versions
The new lazy syntax is Python 3.15 syntax, so a source file containing it cannot be parsed by older Python versions. That matters if your package supports multiple Python releases.
Python 3.15 also provides a transitional **lazy_modules** mechanism. You can declare module names before the relevant imports:
**lazy_modules** = ["json"]
import json
print(json.dumps({"ready": True}))On Python versions that understand the mechanism, the matching import can become lazy. Older Python versions that do not implement it simply ignore the declaration and keep the import eager. This makes the mechanism useful when a project needs a gradual path toward Python 3.15 without immediately putting the new lazy syntax throughout its source.
Measure imports before and after the change
Do not judge the benefit from the source code alone. First identify which imports actually contribute to startup time. Python provides import-time diagnostics that can show how much work individual modules perform while an application starts.
For example, run your application with:
python -X importtime app.pyThe output reports import timing information for the modules loaded during startup. Look for dependencies that consume meaningful startup time but are not needed on every execution path. Those are better candidates for lazy loading than modules that are cheap or universally required.
After changing a suitable import, repeat the same measurement. The useful result is not simply a smaller import-time number; it is a faster startup without creating an unexpected delay later in the workflow.
Know when to force an import back to eager behavior
If a dependency must be initialized before the application reaches a particular stage, keeping it eager is often the safer choice. This is especially true for modules that modify registries, install hooks, configure logging, or perform other setup during import.
Python 3.15 also provides advanced controls for applications that need broader management of lazy imports. The interpreter can be configured with normal, all, or none modes, but these controls are better suited to application and framework authors than ordinary library code. For most projects, explicit lazy imports provide the easier rule: mark only the imports you have measured and understand.
Test the failure behavior before shipping
One subtle change is when errors occur. With an ordinary import, an import failure happens when the import statement executes. With a lazy import, an error associated with loading that dependency can be postponed until the lazy name is first used.
That means your tests should exercise both startup and the code paths that eventually use each lazy dependency. A program that starts successfully is not necessarily healthy if the first real command later discovers a missing package or an import-time configuration problem.
For a project adopting lazy imports, a useful test sequence is simple: start the application, run a command that does not need the lazy dependency, run another command that does need it, and test the failure path where the dependency is unavailable. This catches both startup improvements and delayed errors.
Python 3.15 lazy imports are best used selectively
Python 3.15 makes deferred imports much easier to express than older workarounds because the decision is visible directly beside the import statement. That can reduce startup work while keeping dependencies organized at module level, but it does not remove the need to understand what those dependencies do when they load.
For now, the practical approach is to install the 3.15 release candidate in a test environment, measure your application's imports, select one or two genuinely optional dependencies, and verify their behavior under real execution paths. Python 3.15.0 is scheduled for October 1, giving maintainers a short but useful testing window to find compatibility problems and prepare their packages before the final release.
Written by

