Skip to content
← Back to explainers

Python explainer

Speed Up Startup with Lazy Imports

About 4 min to read. Allow extra time to work through examples.

Python 3.15's lazy import loads a module the first time your code uses it. Learn where you can write it, what moves from startup to first use, and how to control it for a whole program.

This article uses Python. You can make it your practice mode to browse related content across the site.

Your command-line tool imports a large library at the top of its main module. Every run waits for that import, even mytool --help, which never uses the library. To see where startup time goes, run:

python -X importtime -c "import mytool"

Python prints a line for each module it imports, with the time in microseconds.

The usual fix is to move the import into the function that needs it:

def export_report(rows: list[dict[str, str]]) -> str:
    import json   # later calls find json already loaded
    return json.dumps(rows)

That works, but now the import is inside a function, not with the other imports at the top of the file.

Mark an import as lazy

Python 3.15 adds lazy import. The import stays at the top of the file, and Python loads the module the first time your code uses the name:

import sys

lazy import json

print("json" in sys.modules)   # False, nothing loaded yet


def export_report(rows: list[dict[str, str]]) -> str:
    return json.dumps(rows)    # the first use loads json


export_report([])
print("json" in sys.modules)   # True

sys.modules is Python's record of the modules it has loaded. Until the first use, the name json holds a placeholder object of type lazy_import. lazy from json import dumps works the same way, and the first use of dumps loads the whole json module.

You can write lazy only at the top level of a module, including inside an if or a with block there. It is a SyntaxError inside a function, a class or a try block, and with from module import *.

Work moves from startup to first use

A lazy import does the same work later. Three things move with it.

  • Errors. lazy import missing_module succeeds at startup. The ModuleNotFoundError comes at the first use, perhaps in the middle of a request. The traceback shows the line that used the name and the lazy import line.
  • Side effects. Some modules do work when they load, such as a plugin that adds itself to a registry. If your code never uses the name, the module never loads, and that work never happens.
  • Time. The first call that uses the module also loads it, so that call is slower. Measure that call as well as startup.
Check your understanding A plugin module registers itself when it loads, and your app imports it only for that. Can you make that import lazy? Show answer

No. Nothing uses the name, so the module never loads and the plugin never registers. Keep that import eager, or give the plugin a register() function that your app calls. Test startup in a fresh process, because an earlier test may already have imported the plugin and hidden the bug.

Try it without the new keyword

A module can list names in __lazy_modules__. Python 3.15 then treats a plain import of those names as lazy:

__lazy_modules__ = ["json"]

import json   # lazy on 3.15, an ordinary import on 3.12 to 3.14

Older versions see an ordinary list and import json at once, so the file still runs there. The lazy keyword itself is a SyntaxError on Python 3.14 and older, and a version check in the same file cannot hide it. Write Code That Runs on Several Python Versions covers your choices.

Control lazy imports for a whole program

Python 3.15 has two modes:

  • normal, the default. Only the imports you marked are lazy.
  • all. Every import at the top level of a module is lazy, except imports inside a try block.

Choose the mode with -X lazy_imports=all, the PYTHON_LAZY_IMPORTS environment variable, or sys.set_lazy_imports("all"). No mode turns laziness off. Any other value, such as none, makes sys.set_lazy_imports raise ValueError, and stops Python at startup when you pass it with -X or the environment variable.

To keep chosen imports eager, give sys.set_lazy_imports_filter a function. Python calls it for each import that would be lazy, including the ones you marked. It passes the importing module's name, the imported module's name and the names after from ... import. Return False to load that module at once.

import sys


def keep_plugins_eager(
    importer: str | None, name: str, fromlist: tuple[str, ...] | None
) -> bool:
    return not name.startswith("myapp.plugins")


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

Mode all changes every module your program imports, including other people's libraries. After you turn it on, run your whole test suite in a fresh process.

Going further

The lazy keyword comes from PEP 810. To check which mode a program runs in, call sys.get_lazy_imports(), which returns "normal" or "all".

step 21 of 23

Pattern Matching, f-Strings and Newer Syntax

Up next Bring Old Python Code Up to Date