Python 3.15.0 shipped on October 9 — two days ago — and it is the most developer-ergonomics-focused release in years. Four things you have been hacking around manually for a long time are now built in: lazy imports, an immutable dict type, a standard sentinel, and a production-safe profiler. One of those features has a deployment trap that will cost you real production incidents if you ship without reading the fine print.
Lazy Imports: The Feature and the Trap
PEP 810 lands the lazy keyword. The syntax is straightforward:
lazy import json
lazy from pathlib import Path
print("Starting up...") # json and pathlib not loaded yet
data = json.loads('{"key": "value"}') # json loads here
For CLIs with dozens of heavy imports, serverless functions fighting cold start times, or any app that imports pandas and torch at the top level — this is a genuine win. Python has made you pay the full import cost at startup regardless of which code path your process actually runs. That changes with 3.15.
Now the trap. Lazy imports defer ImportError from startup to first access. A deploy goes green. Your readiness probe passes. The process joins the load balancer. The first real request hits the code path that uses the missing module, and you get a 500. Not a crash. Not a restart. The process stays up looking healthy while returning errors on every request that triggers that import. This failure mode is well documented — it converts a deploy-time crash into a production error rate.
Worse: a failed lazy import does not poison the lazy object. It retries on every access. You get the same ImportError on every request until you notice and restart.
The fix is a preflight check before your server accepts traffic:
import importlib
# Force eager load of critical dependencies before accepting requests
for mod in ["stripe", "celery", "redis"]:
importlib.import_module(mod)
There is also a silent failure mode for registry patterns. If a module runs a @register decorator at import time, and you lazy-import it but nothing ever touches the name, the registration never happens — no error, no traceback, just a plugin that is not there. Audit your codebase for import-time side effects before going lazy.
frozendict: Immutable, Hashable, and Actually Usable
PEP 814 adds frozendict as a builtin. The immediately useful part is immutable config and fixed lookup tables — but the hashable part is where it earns its keep:
config = frozendict(env="prod", debug=False, retries=3)
# Use as a dict key or set member
seen = {config: "result"}
# Works directly with lru_cache
from functools import lru_cache
@lru_cache
def get_connection(settings: frozendict):
return db.connect(**settings)
types.MappingProxyType has covered this territory since Python 3.3 but was never hashable. frozendict is hashable when all its keys and values are hashable. One gotcha before upgrading: isinstance(x, dict) returns False for frozendict. Switch to isinstance(x, (dict, frozendict)) or — better — isinstance(x, collections.abc.Mapping).
Tachyon: Profile Production Python Without Slowing It Down
PEP 799 ships the profiling package with two modules: profiling.tracing (deterministic, replaces the deprecated profile module) and profiling.sampling (statistical — the new stuff).
Tachyon samples at up to 1,000,000 Hz with near-zero overhead. cProfile slows your code 2–20x because it hooks every function call. Tachyon attaches to a running process without a restart:
python -m profiling.sampling run script.py
python -m profiling.sampling attach --live 12345 # live view, no restart
python -m profiling.sampling --flamegraph run script.py
Wall clock, CPU, and GIL-holding modes give you different views of where time is going. GIL mode is particularly useful for threading bottlenecks — a question that gets more relevant as free-threading stabilizes. The profile module is now soft-deprecated; migrate to profiling.tracing when you get around to it (removal is 3.17).
sentinel: Retire the _MISSING = object() Pattern
Every Python codebase of any size has this somewhere:
_MISSING = object()
def get(key, default=_MISSING):
if default is _MISSING:
raise KeyError(key)
PEP 661 replaces it with a proper builtin:
MISSING = sentinel("MISSING")
def get(key, default=MISSING):
if default is MISSING:
raise KeyError(key)
The difference: sentinel("MISSING") gives you a clean repr, pickle support, and type-union compatibility. When you combine this with frozendict and lazy import in the same file, 3.15 starts to feel like a proper cleanup release.
The Quiet Wins: UTF-8, JIT, and Comprehension Unpacking
Three more changes worth knowing before you upgrade.
UTF-8 default encoding (PEP 686): open("file.txt") now uses UTF-8 regardless of your OS locale. This eliminates the “works on my Mac, breaks on Alpine Linux in CI” encoding bug — but it silently breaks any code relying on locale encoding. Set PYTHONUTF8=0 or pass encoding="locale" explicitly if you need the old behavior.
JIT improvement: The experimental JIT is 7–8% faster on x86-64 Linux and 11–12% on AArch64 macOS. Still opt-in (python --enable-experimental-jit), still experimental — but this is the first release where the improvement is consistently visible on real workloads.
Comprehension unpacking (PEP 798): You can now unpack sequences inside comprehensions:
[*L for L in nested_lists] # flatten nested lists
{**d for d in list_of_dicts} # merge dicts (later keys win)
Upgrade Checklist
Before you pin 3.15 in production, check these:
- Any
open()calls withoutencoding=now use UTF-8 — test against non-UTF-8 data. - Code using
isinstance(x, dict)will missfrozendictinstances. load_module()is removed from Loader — migrate toexec_module().- NamedTuple keyword-argument constructor syntax is gone — use class or functional form.
__cached__attribute on modules is removed — use__spec__.cached.- If using lazy imports in a server: add a preflight check to force-load critical deps before accepting traffic.
Python 3.15 is a release that charges you in test time what it pays back in production quality. The full changelog is worth a read, but these six items cover the bulk of what will affect your day-to-day code. The lazy import gotcha deserves a second read before you ship — the rest of it is just the standard library catching up to what everyone was doing by hand.













