Developer Tools

Python 3.15 Lazy Imports: Which Mode Pays

Python 3.15 shipped PEP 810 lazy imports on 9 October 2026. The keyword is safe and slow; the global all mode is where the 50-70% startup numbers come from.

TechLogHub Editorial
October 11, 2026
8 min read
0 views

Share Article

Dark cover reading Python 3.15 Lazy Imports with three rows for the normal, all and none modes.

Python 3.15 Lazy Imports: Which Mode Pays

Quick answer: Python 3.15 shipped on 9 October 2026 with lazy imports (PEP 810). The per-statement lazy import keyword is safe and saves you almost nothing by itself. The 50–70% startup numbers in the PEP come from the global -X lazy_imports=all mode — which PEP 810 explicitly refused to make the default and tells library authors not to use. The feature is real. The win requires you to accept a behaviour change the PEP would not impose on everyone.

Python 3.15.0 is out, and the headline feature is the one Python has turned down for a decade: imports that do not run until you touch the name. PEP 810 is Final, created 2 October 2025 and resolved 3 November 2025, so this shipped roughly eleven months after acceptance.

Most coverage will stop at “Python has lazy imports now.” The useful question is which of the three modes to turn on, and the answer differs for a CLI, a library and a web service.

The syntax, and where it is a SyntaxError

Two forms, with lazy as a soft keyword:

lazy import json
lazy from pathlib import Path

data = json.loads(payload)   # json is imported here

PEP 810 restricts this hard: “The soft keyword is only allowed at the global (module) level.” Inside a function, inside a class body, inside a try block, or on import *, it is a syntax error — not a silent fallback. from __future__ imports cannot be lazy either, and imports that cannot be lazy are always eager.

The from X import Y behaviour is the detail people will get wrong. Each name is bound to a proxy, and the first access to any one of them loads the whole module; only the accessed name is reified. So lazy from pathlib import Path, PurePath is not two independently deferred objects — it is one deferred module import with two entry points.

The three modes

ModeWhat is lazyWho it is for
normal (default)Only imports you marked with the lazy keyword.Everyone. This is what you get with no configuration.
allEvery module-level import becomes potentially lazy, except those inside try blocks and star imports.Application and framework authors who control the whole process.
noneNothing. Even explicit lazy statements are eager.Installers, and anything that needs import order to be exactly as written.

You set the mode three ways, and the precedence order is sys.set_lazy_imports(), then -X lazy_imports=<mode>, then PYTHON_LAZY_IMPORTS=<mode>. There is also a module-level __lazy_modules__ list, which names modules to treat as lazy on 3.15 and above and is ignored on older versions — the one mechanism here that is safe to ship in a library targeting multiple Pythons.

“Potentially lazy” is doing work in that table. In all mode an import is marked as a candidate, and a filter decides. sys.set_lazy_imports_filter(func) takes func(importer, name, fromlist) -> bool; with no filter set, every potentially lazy import is lazy. That filter is the part of this feature most worth knowing about, because it is how you get the all-mode speedup while forcing the three modules that actually need eager import to stay eager.

The numbers, and where they come from

PEP 810’s Motivation states that lazy imports “can reduce startup time by 50-70% in practice” and that “memory savings of 30-40% have been observed in real workloads.” Worth being precise about the provenance: the PEP gives those two figures without naming the tool or application behind them. The one named-application number sits further down, under Backwards Compatibility, and it is smaller — a 10–20% startup improvement for PySide applications.

So calibrate by workload, not by the headline. Import cost concentrates in a handful of heavy packages. A CLI that imports a data-science stack to parse one argument is the 50–70% case. A long-lived web worker that imports everything during boot and then serves for days gains nothing on throughput and only moves work around — possibly into the first request, which is worse than during boot. Measure first; 3.15’s new profiling package (PEP 799) ships a sampling profiler for exactly this.

What actually breaks

PEP 810 lists the consequences plainly, and they are all variants of one thing: the import no longer happens where you wrote it.

Import-time side effects move

The PEP says “users should be wary of modules that rely on import time side effects,” and names registry patterns as the common example. If importing a module is how your plugins register themselves, a lazy import means they register at first attribute access — which may be after the code that iterates the registry. This is the single biggest source of breakage and it fails silently, as an empty list rather than an exception.

Errors arrive late

An ImportError or AttributeError surfaces at first use, not at the import statement. PEP 810 does mitigate this well: exception chaining shows both the definition site and the access site, with messages such as “lazy import of ‘json.dumsp’ raised an exception during resolution.” And “exceptions during reification prevent the replacement of the lazy object,” so a later access retries rather than returning a poisoned proxy.

sys.modules is not the whole picture

“Lazy module objects do not appear in sys.modules.” They live in sys.lazy_modules until reification. Any code that introspects sys.modules to decide whether a dependency is present — common in optional-dependency shims — now has to check both, and before first use introspection can show a proxy object.

Threads pick the importer

A module may be imported by whichever thread first touches the lazy name. If your module-level import was implicitly happening on the main thread and something in that import is not thread-safe, that assumption is gone.

The security note nobody will read

Buried in PEP 810’s Security Implications is advice for one audience: tools that install packages into the same environment they import from should import or reify modules before installation, so new packages cannot shadow them. The PEP suggests mode none for this.

If you build or vendor anything that installs packages — a plugin manager, a notebook bootstrap, a deploy tool that pip-installs at runtime — that paragraph is a requirement, not a footnote. Same class of problem as everything else in the install path: see what ends up inside a published package for the JavaScript equivalent.

The sleeper change in 3.15 is not lazy imports

While everyone writes about PEP 810, PEP 686 landed too: open() without an encoding argument now defaults to UTF-8 instead of the locale encoding. On Linux and macOS most people were already effectively on UTF-8 and will notice nothing. On Windows, where the locale encoding was typically a code page, this changes what existing code reads and writes. PYTHONUTF8=0 or -X utf8=0 restores the old behaviour.

That is the change most likely to produce a confusing bug report in a cross-platform project this quarter, and it needs no opt-in. Also in 3.15: frozendict (PEP 814, deliberately not a dict subclass), sentinel (PEP 661), unpacking in comprehensions (PEP 798), math.integer (PEP 791), re.prefixmatch() with re.match() soft-deprecated in its favour, and abi3t, a stable ABI target for free-threaded builds. Two to grep for: __cached__ is no longer set by the import system, and profile is deprecated for removal in 3.17.

Recommended posture

Libraries: use __lazy_modules__ and do not touch the global mode — PEP 810 discourages library authors from setting it because it changes how the consuming application runs. Applications and CLIs: this is your feature. Turn on all, measure startup, then filter the modules whose side effects you depend on. Installers: none, per the PEP’s own security guidance.

Note the support window too. Per PEP 790, 3.15.0 is final as of 9 October 2026, bugfix releases run roughly two years, source-only security fixes for three years after that, and end of life lands around October 2031, with Hugo van Kemenade as release manager. A five-year horizon is longer than most JavaScript runtimes offer — we made the comparison the other way round when Node started stripping types without a build step, and the stdlib naming debate resurfaced in the proposed std namespace.

Auditing which dependencies are safe to defer starts with the ones that do work at import time. Our developer tools directory and free browser tools are worth a pass if you are rebuilding a toolchain around 3.15.

FAQ

When was Python 3.15 released?

9 October 2026. The python.org release page states it is the stable release of 3.15.0, and PEP 790 schedules 3.15.0 final for Friday 2026-10-09. End of life is approximately October 2031.

Are lazy imports on by default in Python 3.15?

No. The default mode is normal, which defers only imports you explicitly write as lazy import or lazy from ... import. PEP 810 states that changing import to be lazy by default was outside its scope, and it was listed among the rejected ideas.

How do I make every import lazy?

Run with -X lazy_imports=all, set PYTHON_LAZY_IMPORTS=all, or call sys.set_lazy_imports('all'). Precedence runs in that reverse order: the sys call wins, then -X, then the environment variable. Do this in applications, not libraries.

Can I write lazy imports inside a function?

No. PEP 810 allows the soft keyword only at module level, and raises a syntax error inside functions, class bodies and try blocks, and on import *. Function-level imports already give you deferral; the keyword exists to make module-level imports deferrable.

Will lazy imports break my plugin registry?

Possibly, and quietly. If registration happens as an import side effect, deferring the import defers the registration. PEP 810 names registry patterns explicitly and warns about modules that rely on import-time side effects. Force those modules eager, either by importing them normally or with a lazy-imports filter.

Does lazy importing help a long-running web server?

Much less than it helps a CLI. Startup savings are one-time in a process that runs for days, and deferring an import can move that cost into the first request. The clearer win is memory, for which the PEP cites 30–40% savings in unnamed real workloads.


Lazy imports are the rare Python feature where the safe version and the fast version are different switches. Pick the one that matches what you ship.

Stay Updated

Get the next deep dive in your inbox

Subscribe for product analysis, engineering explainers, and practical guides published on TechLogHub.

See what launched this week

One email a week: new and trending developer tools, fresh comparisons, and what shipped. Unsubscribe in one click.