Python 3.15 Brings Lazy Imports, and Your Slow Startup Might Thank You

Last updated: Mon. Oct. 5, 2026

Anyone who has worked on a large Python application knows the feeling of typing the run command and then waiting. Part of that wait is usually the import system, and Python 3.15 adds a feature aimed squarely at it: lazy imports.

Python 3.15 is not out yet, so details could still shift before release. Everything below follows the current documentation.

Why startup gets slow

Importing a module is more work than it looks. Python has to find the file, read it from disk, compile it to bytecode, and then run every line of its top-level code. Do that across a deep tree of dependencies and you can lose whole seconds before your program does anything useful.

The annoying part is that much of that work is often wasted. A given run may touch only a small slice of the modules you imported, but you paid to load all of them.

The workarounds we have all used

Most of us have dealt with this by hand. You move an import inside the function that needs it. You reach for importlib and load things on demand. Or you reshuffle the code so heavy dependencies are not pulled in so early.

These tricks work, but they come with a cost. Imports end up scattered around the codebase instead of sitting in one predictable place at the top. The code gets harder to read, and keeping the habit consistent across a team takes real discipline.

The new lazy keyword

Python 3.15 offers a cleaner option: put the soft keyword lazy in front of an import. Python then holds off on loading the module until the moment you first use the name. You keep every import neatly at the top of the file, and you only pay for the modules your program actually touches.

It works with both import and from ... import. Under the hood, Python creates a lightweight proxy object in place of the module. The real loading happens quietly the first time you access that name.

lazy import json
lazy from pathlib import Path

print("Starting up...") # neither module has been loaded yet

data = json.loads("{}") # json is loaded here
here = Path(".") # pathlib is loaded here

This is most useful for programs that import a lot at the top level but use only part of it on any one run. Startup gets faster, and you do not have to restructure anything or sprinkle conditional imports through your code.

When a lazy import fails

If a lazily imported module cannot be loaded, say because it does not exist, the error does not appear at import time. It appears at the point where you first use the name.

That might sound like it would make debugging harder, but the traceback helps. It shows both the line where you used the name and the original import statement, so you can see where things went wrong and where the import was declared.

Turning it on without touching your code

You can also switch lazy loading on globally. Python gives you the -X lazy_imports command-line option and the PYTHON_LAZY_IMPORTS environment variable. Both take one of two values:

  • all makes every import lazy by default.

  • normal, the default, only honors the lazy keyword where you wrote it.

If you want to check or change the mode while the program is running, there are sys.get_lazy_imports() and sys.set_lazy_imports().

Choosing which modules go lazy

For finer control, sys.set_lazy_imports_filter() takes a function that decides, module by module, whether an import may be lazy. Python calls it with three arguments: the name of the importing module (or None), the name of the module being imported, and the fromlist (or None for a plain import). Return True to allow laziness and False to force a normal, eager load.

A handy pattern is to make only your own code lazy while third-party packages load as usual:

import sys

def only_my_code(importing, imported, fromlist):
return imported.startswith("shop.")

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

import shop.reports # lazy, matches the filter
import json # eager, does not match

If you ever need to detect a lazy import in your own code, the proxy type is exposed as types.LazyImportType.

The rules you need to know

There are a few limits on where lazy can appear:

  • It only works at module scope. Using it inside a function, a class body, or a try/except/finally block raises a SyntaxError.

  • Star imports cannot be lazy.

  • Imports from future cannot be lazy.

Breaking any of those gives you a SyntaxError, so you will hear about it immediately.

Supporting older Python versions

What if your project still has to run on versions older than 3.15? The lazy keyword would be a syntax error there, so you cannot use it directly. The alternative is to define __lazy_modules__ at the top of a module, a list of fully qualified module names. Ordinary import statements for those modules then behave exactly as if you had written lazy:

__lazy_modules__ = ["json", "pathlib"]

import json # lazy
import os # still eager

Should you care?

If your application starts quickly already, you may never need this. But if you maintain a command-line tool, a big web app, or anything with a heavy dependency tree, lazy imports offer a tidy way to cut startup time without making your code uglier. That is a trade I have wanted for a long time.

I will be trying it on one of my own slower projects once 3.15 lands. If you do the same, I would love to hear how much time it saves you.

« Home