BuildNot yet confirmed elsewhere1 publisher2 min readPublished
Python 3.15 adds a lazy import keyword that defers each module until first use
Python 3.15, released 9 October 2026, adds a lazy soft keyword that waits to load a module until code first touches its name. CLIs and test runners start faster, and a missing or misspelled module now raises only when that code path first runs.
The Engineer · Build desk
What happened
- The same release makes UTF-8 the default encoding, adds two builtin types and a sampling profiler, and allows * and ** inside comprehensions, per a dev.to walkthrough.
- Lazy loading can be set globally with -X lazy_imports or PYTHON_LAZY_IMPORTS, and the default mode, normal, honours only the lazy keyword.
- A program can switch laziness at runtime, module by module, through sys.set_lazy_imports_filter().
- The new frozendict builtin cannot be modified after creation and is hashable when its contents are, so it can be a dict key or a set member.
Why it matters
- constraint Optional-dependency guards written as try/except ImportError cannot use the keyword on those lines, because lazy inside a try block is a SyntaxError.
- exposure A dependency missing from a production image can stay hidden through startup and fail later, the first time a rarely used path runs, unless tests exercise that path.
- decision Libraries that still support pre-3.15 interpreters have to pick the __lazy_modules__ list over the keyword to get faster startup without dropping older versions.
- cost Code that gates on isinstance(x, dict) has to be edited before it will accept a frozendict, and config loaders are the likely place to hit it.
Until now, deferring an import meant moving the statement inside the function that used it and hoping the linter stayed quiet [3]. With the 3.15 keyword, imports stay at the top of the file and each module loads on first use [1]. In the walkthrough's example, `lazy import json` and `lazy from pathlib import Path` sit above a print call that loads neither module. json loads at the `json.loads` call and pathlib at `Path(".")` [4].
How much a project saves depends on which modules each run touches. If a run never reaches the code that needs json, json never loads [2]. A CLI whose `--help` path touches none of its heavy dependencies gets the full saving. One whose help path imports them anyway saves nothing. The walkthrough names CLI tools, serverless functions and test runners as the main beneficiaries [17], and it labels the load costs in its own diagram as illustrative [12]. We'd use the sampling profiler that ships in the same release [6] to find where a given tool's startup time goes before rewriting any imports. The walkthrough text available for this report lists the UTF-8 default and the removals without saying what they break.
The breakage it does document comes from laziness itself. A misspelled or missing module no longer raises at the import line. It raises at first use, though the traceback still includes the original import line [10]. The keyword is legal only at module scope. Inside a function, a class, or a try/except/finally block it is a SyntaxError, and star imports and `__future__` imports cannot be lazy [11].
For projects that still support older interpreters, the walkthrough points to a module-level list: `__lazy_modules__ = ["json", "pathlib"]` makes plain import statements for those modules lazy [9]. We'd expect libraries to adopt that form before the keyword. It is the one the walkthrough recommends for code that must keep running on older versions [9].
The second documented trap is in a new builtin. frozendict inherits from object, so `isinstance(x, dict)` returns False for it, and the walkthrough suggests testing against `collections.abc.Mapping` instead [14]. Its equality ignores insertion order, so two frozendicts with the same items compare equal however they were built [15]. The cleanest use is configuration. Passing `object_pairs_hook=frozendict, array_hook=tuple` to `json.loads` gives a parsed document that cannot be mutated after loading [16].
What to watch
- Measured startup times from CLI and test-runner projects that adopt lazy imports, replacing the walkthrough's illustrative figures.
- Bug reports from teams moving to 3.15 whose code relied on the previous default encoding.
- Whether -X lazy_imports=all becomes a common setting in serverless images and CI configurations.
Clarity's read
What the record supports and how the coverage leans. The claims behind it follow.
Reality
- Evidence45
- Adoption
- Insufficient
- Hype gap+15
- Incentives
- Insufficient
- Confidence55
Claim ledger
Ranked by verification strength, evidence, and original report placement.
- [1]
A new lazy soft keyword tells Python not to load a module until it is used; imports are still written at the top of the file and Python does not pay for them until the name is touched.
ReportedSupportedSource: dev.to walkthrough2 sources— create a free account to open themView cited source - [2]
If a run never reaches the code that needs json, json never loads.
ReportedSupportedSource: dev.to walkthrough2 sources— create a free account to open themView cited source - [3]
The usual previous fix for slow startup was to put import statements inside functions and hope the linter did not complain.
ReportedSupportedSource: dev.to walkthrough2 sources— create a free account to open themView cited source - [4]
In the walkthrough's example, after 'lazy import json' and 'lazy from pathlib import Path', a print call runs with neither module fully imported; json loads at json.loads and pathlib loads at Path(".").
ReportedSupportedSource: dev.to walkthrough2 sources— create a free account to open themView cited source - [5]
Python 3.15 was released on 9 October 2026.
- [6]
Python 3.15 includes a lazy keyword for imports, two new builtin types, UTF-8 as the default encoding, a sampling profiler in the standard library, * and ** inside comprehensions, JIT progress, and a set of removals.
- [7]
Lazy imports can be set globally with -X lazy_imports=all|normal or the PYTHON_LAZY_IMPORTS environment variable; normal is the default and only honours the lazy keyword.
- [8]
Runtime control is available through sys.set_lazy_imports(), sys.get_lazy_imports(), and sys.set_lazy_imports_filter() for per-module decisions.
- [9]
Setting __lazy_modules__ = ["json", "pathlib"] makes plain import statements for those modules lazy; the walkthrough calls this the backport-friendly form to use when still supporting older Pythons.
- [10]
With lazy imports, errors show up at first use rather than import time; a typo'd or missing module fails later, and the traceback includes the original import line.
- [11]
lazy only works at module scope; inside a function, a class, or a try/except/finally it is a SyntaxError, and star imports and __future__ imports cannot be lazy.
- [12]
The walkthrough says the load costs in its lazy-import diagram are illustrative and should not be quoted.
- [13]
A frozendict cannot be changed after creation and is hashable as long as its contents are hashable, so it can be used as a dict key or placed in a set.
- [14]
frozendict is not a dict subclass and inherits from object, so isinstance(x, dict) fails for it; the walkthrough suggests isinstance(x, (dict, frozendict)) or checking against collections.abc.Mapping.
- [15]
frozendict remembers insertion order but equality ignores order, so two frozendicts with the same items are equal regardless of construction order.
- [16]
json.loads(..., object_pairs_hook=frozendict, array_hook=tuple) gives a fully immutable parsed document.
- [17]
The walkthrough says lazy imports matter most for CLI tools, which pay the full import cost just to print --help, and for things that start often, such as serverless functions and test runners.
Sources
1 independent publisher whose own reporting we read for this story.
- dev.toPython 3.15 just dropped. Here's everything that changed from 3.14
1 article · October 10, 2026
Topics and entities
Follow any of these and your For You feed starts watching them — no settings page required.