PEP 790 schedules Python 3.15.0 final for 2026-10-09. The official What's New is the right place for the full inventory, but it is a long page to carry into a planning conversation. This post is the five changes I would check before bumping a runtime -- language and stdlib shifts that show up in day-to-day work, not the complete changelog.
Explicit lazy imports#
Large apps often pay for imports they never touch on a given run. People already work around that by burying imports inside functions or wiring importlib by hand. That works, and it also scatters the dependency graph.
I have lived that tradeoff on the Open World Holidays Framework (holidays on PyPI). In 2023 I merged entity lazy loading so import holidays would not pull every country entity up front. Entity load was step one; as of this writing, package __init__ eagerness is still open work, with two external PRs aimed at lazy country and financial modules and a follow-up for calendars, groups, and version. That is the same family of startup costs PEP 810 is trying to shrink.
PEP 810 adds an explicit lazy soft keyword. Mark an import as lazy and Python installs a proxy; the module loads on first use:
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.
p = Path(".") # pathlib loads here.There is also a global mode (-X lazy_imports=all / PYTHON_LAZY_IMPORTS) and a filter via sys.set_lazy_imports_filter() if you want only your package tree to defer. Lazy imports are module-scope only -- not inside functions, class bodies, or try / except -- and star imports cannot be lazy. For dual-version code, __lazy_modules__ can mark names that should behave as lazy when the keyword is not available yet.
UTF-8 as the default encoding#
PEP 686 makes UTF-8 the default encoding independent of the process locale. I/O without an explicit encoding uses UTF-8 on every platform. That closes a long-running class of "works on my Linux box, misreads on Windows" bugs.
with open("notes.txt", encoding="utf-8") as f:
text = f.read()with open("notes.txt") as f:
text = f.read()You can restore the previous behavior with PYTHONUTF8=0 or -X utf8=0 while you port. Prefer explicit encodings at trust boundaries anyway; the default just stops being a locale lottery.
Unpacking in comprehensions#
PEP 798 extends PEP 448-style * / ** unpacking into list, set, and dict comprehensions (and generator expressions). That is a readable alternative to nested comprehensions or itertools.chain.from_iterable() when you are flattening one level:
lists = [[1, 2], [3, 4], [5]]
flat = [x for xs in lists for x in xs]
assert flat == [1, 2, 3, 4, 5]
dicts = [{"a": 1}, {"b": 2}, {"a": 3}]
merged = {k: v for d in dicts for k, v in d.items()}
assert merged == {"a": 3, "b": 2}lists = [[1, 2], [3, 4], [5]]
flat = [*xs for xs in lists]
assert flat == [1, 2, 3, 4, 5]
dicts = [{"a": 1}, {"b": 2}, {"a": 3}]
merged = {**d for d in dicts}
assert merged == {"a": 3, "b": 2}Small change, high frequency once you know it exists. For the dict form, later mappings win on key collisions -- same as {**a, **b} outside a comprehension.
Profiling package#
PEP 799 puts Python's built-in profilers under a single profiling package. Deterministic function-call tracing lives at profiling.tracing (relocated from cProfile, which stays as a compatibility alias). The new piece is Tachyon, a statistical sampling profiler at profiling.sampling.
Unlike tracing profilers that instrument every call, Tachyon periodically samples stacks. You can attach to a running process by PID without restarting it, run a script or module from the start, or take a one-shot dump -- including asyncio-aware dumps. Modes cover wall time, CPU time, GIL-holding time, and exception-handling time.
python -m profiling.sampling attach <pid>
python -m profiling.sampling run myscript.pycProfile keeps working. The older profile module is deprecated and scheduled for removal in 3.17 -- prefer profiling.tracing. For production performance work, attach-without-restart is the change that matters most. Details: What's new -- profiling package.
frozendict#
PEP 814 adds an immutable mapping built-in. A frozendict is not a dict subclass. It preserves insertion order, compares without caring about order, and is hashable when keys and values are hashable:
f = frozendict(x=1, y=2)
assert f == frozendict(y=2, x=1)
assert hash(f) == hash(frozendict(y=2, x=1))
f["x"] = 42
# TypeError: 'frozendict' object does not support item assignment
f.update({"z": 3})
# AttributeError: 'frozendict' object has no attribute 'update'
f.pop("x")
# AttributeError: 'frozendict' object has no attribute 'pop'
del f["x"]
# TypeError: 'frozendict' object does not support item deletionThe practical win is for values you want to treat as fixed -- a config snapshot, a cache key, or an API argument that should not be mutated after you hand it off. Before 3.15 that usually meant MappingProxyType plus a comment asking callers not to poke at the underlying dict. frozendict puts the same idea in builtins, with hashing when the contents allow it. One upgrade note: it is not a dict subclass, so isinstance(x, dict) will miss it. Prefer collections.abc.Mapping when you mean to accept either, or check for both types explicitly.
What I left out#
Plenty else in 3.15 may still matter for your stack -- sentinel, the JIT, typing PEPs, clearer errors, and packaging details such as free-threading on the official macOS installer (enabled by default there; still opt-in on Windows and a separate build elsewhere). The full inventory is in What's new in Python 3.15. When you are ready to bump, skim Porting to Python 3.15 for the behavior changes that can bite.
After this post went up on October 1, 2026, Python shipped a surprise 3.15.0rc3 on October 2. Last-minute lazy-import release blockers still needed to land in the final, with a little time to test them, so PEP 790 moved 3.15.0 final from 2026-10-01 to 2026-10-09. The release notes call it the last planned release candidate (about 156 bug fixes, build improvements, and documentation changes from 82 contributors since the previous candidate), with no ABI changes from this point in the 3.15 series.
References#
- Open World Holidays Framework
- PEP 686 UTF-8 mode default
- PEP 790 Python 3.15 release schedule
- PEP 798 unpacking in comprehensions
- PEP 799 profiling package
- PEP 810 explicit lazy imports
- PEP 814 frozendict
- Porting to Python 3.15
- Python 3.15.0rc3 release
- What's new in Python 3.15 profiling package
- What's new in Python 3.15 release highlights


