Python 3.15 Lazy Imports: What to Defer in a Slow CLI
A plain --help on your CLI spends its time importing modules it never uses. Which imports are safe to defer in Python 3.15, and which ones break plugins?

- Python 3.15 can defer a module until an imported name is first used.
- Dependencies used by only some commands are candidates; required startup registration needs to run on time.
- Test real commands and missing dependencies as well as help output.
Start with dependencies the help path never uses
A CLI can spend time loading a subsystem even when the user only asks for help. Python 3.15, released October 9, adds explicit lazy imports: loading waits until the imported name is needed. A command that never touches that name may avoid the work altogether.
Choose candidates by their role in startup. These are practical distinctions for a CLI:
| Dependency’s role | Decision |
|---|---|
| File handling used by one optional command | A candidate for deferral until that command runs. |
| Argument parsing required immediately on every run | Deferral is unlikely to avoid its loading cost. |
| Populating a plugin registry during import | Ensure it runs before the registry is consumed. |
Keep the declaration at module level
This example uses zipfile only when the user requests an archive listing. The help path does not reference it.
import argparse
lazy import zipfile
parser = argparse.ArgumentParser()
parser.add_argument("--list-zip")
args = parser.parse_args()
if args.list_zip:
with zipfile.ZipFile(args.list_zip) as archive:
print("\n".join(archive.namelist())) The module is loaded when that branch first uses its name, unless other code has already imported it. The language reference also permits lazy from package import name. Using one imported member still loads the whole module.
The new syntax belongs at module scope. It cannot appear inside a function, class body or try/except/finally block. Existing ordinary imports inside functions remain a different option.
A plugin import may be doing more than binding a name
Some modules register plugins as their top-level code runs. Deferring such an import also postpones registration. If the program reads a registry without ever touching the imported module’s name, the registration it expects may not have happened.
PEP 810 recommends explicit initialization functions for these patterns. Keeping a necessary import eager is also an option in the default mode when restructuring initialization is outside the change.
A missing or broken dependency can likewise fail at first use. Successful help output therefore does not establish that a command’s optional dependencies will load. Exercise the branch that actually uses them and check how its failures are handled.
Compare the help path with a real operation
CPython’s -X importtime option reports self time and cumulative time including nested imports. With the example saved as cli.py, compare these paths, substituting an existing archive for sample.zip:
python3.15 -X importtime cli.py --help
python3.15 -X importtime cli.py --list-zip sample.zip Compare total command duration separately from import timings. A faster entry point can still leave the first actual operation paying the deferred cost. Include registry-dependent behavior and a missing-dependency case in the checks.
Global mode, -X lazy_imports=all, also changes ordinary imports. The default normal mode preserves ordinary import behavior while honoring explicit lazy declarations. For a targeted change, local declarations make the scope easier to inspect.
Code supporting older Python versions can instead declare __lazy_modules__ = ["zipfile"] before an ordinary import zipfile. Python 3.15 and later defer that import; older versions run it eagerly. This avoids putting unsupported syntax in shared source files. Adopt the change when both the shorter startup path and the real commands behave as intended.