Python 3.15 shipped on October 9. The headline feature is explicit lazy imports — a syntax change that defers module until first use. Meta’s codebase saw a 70% reduction in initialization time after adopting the pattern. The release also ships frozendict as a built-in type, a proper sentinel, unpacking in comprehensions, and a JIT compiler that is finally showing numbers worth quoting. Here is what matters and what to watch.
Lazy Imports: The Startup Fix Python Needed #
For years, heavy Python apps had a startup problem. A web server importing Django, SQLAlchemy, and a handful of ML libraries could spend 400–800ms doing nothing but running import statements before accepting a single request. The standard fix was manual workaround: move imports inside functions, use importlib tricks, or fork CPython like Meta did. Python 3.15 makes this a first-class feature.
The new lazy soft keyword defers module until the imported name is first accessed:
lazy import json
lazy from pathlib import Path
data = json.loads('{"key": "value"}')
For projects that need to stay compatible with older Python versions, there is a backwards-compatible approach using the __lazy_modules__ list. On Python 3.15+, those imports become lazy. On older versions, the list is ignored and imports proceed normally.
__lazy_modules__ = ['numpy', 'pandas', 'heavy_analytics_lib']
import numpy
import pandas
The practical wins are real. CLI tools are the clearest example — when a user runs --help, there is no reason to import a plotting library or a database driver. Meta’s 70% initialization reduction was not a lab benchmark; it came from converting production libraries with deep dependency graphs.
Two risks worth naming. First: late error detection. An ImportError that used to surface at startup will now surface when you first access the lazy import, potentially during request handling or a background job. Second: thread safety. Startup was previously single-threaded; now any thread that first touches a lazy import triggers the load. Neither is a dealbreaker, but both require awareness when adopting this pattern in concurrent systems.
frozendict: A Built-in That Took 20 Years #
Python developers have needed an immutable, hashable dictionary for a long time. The standard library offered types.MappingProxyType as a partial answer, but it was a view over a mutable dict — not genuinely hashable, not usable as a dict key. Third-party packages filled the gap. Python 3.15 ships frozendict as a proper built-in via PEP 814.
import functools
config = frozendict({"model": "gpt-4o", "temperature": 0.7})
@functools.lru_cache
def run_query(cfg: frozendict) -> str:
...
seen_configs = {frozendict(user="alice"), frozendict(user="bob")}
lookup = {frozendict(scope="read"): handler_fn}
A few things to know before adopting it. frozendict is not a dict subclass — isinstance(fd, dict) returns False. That is intentional: it avoids inheriting mutation methods that would need to raise errors. The immutability is also shallow: a nested list inside a frozendict is still mutable, and a frozendict containing a list cannot be hashed. These are the same constraints that apply to frozenset, so the behavior is at least consistent.
The JIT Keeps Compounding #
Python’s experimental JIT has been technically available since 3.13. It has not been worth enabling until now. Python 3.15 shows 7–8% geometric mean improvement on x86-64 Linux and 11–12% on AArch64 macOS, with upgrades to LLVM 21, a new tracing frontend, basic register allocation, and lower memory usage for generated machine code.
The trajectory matters more than any single number: roughly 2% in 3.13, 4–5% in 3.14, 7–8% in 3.15. The JIT is still experimental and opt-in — it will not activate automatically. It is also not PyPy. But 8% on a distributed workload has real infrastructure cost implications, and the trend line is clearly heading somewhere.
Smaller Wins Worth Knowing #
Python 3.15 also ships sentinel as a built-in. The old _MISSING = object() pattern works fine but produces ugly reprs and does not pickle cleanly. sentinel("MISSING") gives you a named, picklable, identity-preserving sentinel that works properly with type annotations.
PEP 798 adds unpacking to comprehensions:
flat = [x for sublist in lists for x in sublist]
flat = [*sublist for sublist in lists]
merged = {**d for d in dict_list}
What Actually Breaks #
UTF-8 is now the default encoding on all platforms. Code that calls open() without an explicit encoding= argument and relies on locale defaults — especially on Windows — may produce different output. The fix is to pass encoding="utf-8" explicitly.
Several long-deprecated APIs are now removed: datetime.utcnow() (use datetime.now(datetime.UTC)), importlib.resources.read_text(), and the internal sre_compile, sre_constants, sre_parse modules. The regex internals are the most likely surprise — some packages accessed them directly. Check your dependencies before upgrading. The official What’s New page has the full list.
Should You Upgrade? #
For greenfield projects: yes. The new built-ins are cleaner than their alternatives, and lazy imports are worth adopting for anything with non-trivial startup cost. For existing projects: check your dependencies first. The UTF-8 default and removed APIs are the likeliest sources of breakage, and most projects will need small fixes rather than rewrites.
Python 3.15 is not a headline release — there is no walrus operator moment, no match statement. What it is: a release that fixes chronic annoyances that Python developers have been patching around for years. That is often more valuable than a flashy syntax addition. The official download is available now.