
Python 3.15 RC1 dropped on August 4. The release candidate tag matters for one practical reason: the ABI is frozen. Wheels you build against rc1 will run on the final October release without recompilation. That makes right now the window for testing — teams that skip it will spend October firefighting encoding bugs instead of shipping.
Lazy Imports Are the Headline — and They Deserve the Attention
PEP 810 introduces a lazy keyword that defers module loading until the imported name is actually used. The performance case is real: on large codebases with sprawling import graphs, Python core developers have measured startup time reductions up to 70%. CLI tools, data pipelines, and web frameworks that import half a dependency tree on cold start will feel this immediately.
The syntax is clean:
lazy import json
lazy from pathlib import Path
print("starting...") # neither json nor pathlib loaded yet
data = json.loads('{"key": "value"}') # json loads here
If you need backward compatibility, the __lazy_modules__ attribute handles it without changing individual import lines:
__lazy_modules__ = ["json", "pathlib"]
import json # now lazy
import os # still eager
The design is deliberately explicit and opt-in. No silent global flag, no implicit behavior change. That said, two tradeoffs are worth understanding before you reach for it everywhere.
First, import errors defer to first use, not load time. A typo in a module name or a missing dependency will not surface at startup — it will surface in whatever code path first touches the name. In long-running processes or background workers, that could be hours into execution.
Second, in multithreaded code, the actual import (and any side effects it carries) may execute in an unexpected thread. If a module registers signal handlers or modifies global state on import, lazy-loading it means that happens in whatever thread gets there first. For most pure utility modules this is a non-issue; for anything with side-effectful imports, audit before enabling.
The community has been debating this for years — an earlier discussion of PEP 810 broke 350 points on Hacker News. The concerns are legitimate but the feature is sound. Explicit is better than implicit, and 70% faster startup is worth knowing how to use correctly.
frozendict Is Finally a Built-In
PEP 814 closes a gap that has been annoying Python developers for over a decade. frozendict is now a proper built-in type — immutable, hashable when its values are hashable, and a first-class citizen alongside frozenset.
The previous workaround, types.MappingProxyType, was read-only but not hashable and could not be subclassed. That meant you could not use it as a dictionary key, a set element, or an argument to @functools.lru_cache(). frozendict handles all three:
import functools
@functools.lru_cache(maxsize=None)
def process(config: frozendict) -> str:
return config["mode"]
cfg = frozendict({"mode": "fast", "threads": 4})
process(cfg) # cacheable — frozendict is hashable
Practical use cases land immediately: frozen configuration objects in microservices, thread-safe shared state that does not need a lock, and function default arguments that cannot be accidentally mutated. If your codebase has any MappingProxyType usage, start planning the migration.
UTF-8 Is Now the Default — Windows Developers, Act Now
PEP 686 enables UTF-8 mode globally in Python 3.15. On Linux and macOS, this changes nothing — UTF-8 was already the system default. On Windows, where the active code page is often cp1252, cp932, or a regional variant, every open() call without an explicit encoding= argument now behaves differently.
The failure mode is ugly: UnicodeError if you are lucky, mojibake or silent data corruption if you are not. The fix is straightforward but tedious — explicitly set the encoding on every text-mode file operation:
# Before (breaks on Windows after 3.15)
with open("data.txt") as f:
content = f.read()
# After (correct everywhere)
with open("data.txt", encoding="utf-8") as f:
content = f.read()
Python 3.10 added EncodingWarning specifically for this migration. Run your test suite now with -W error::EncodingWarning to surface every call site that omits encoding=. The -X utf8=0 flag is a temporary stopgap for production while you fix them, but it is not a long-term answer.
Two More Worth Knowing
The JIT compiler gets another significant upgrade in 3.15 — 8 to 13 percent geometric mean improvement depending on platform, with 50 percent better code coverage than 3.14. We covered the JIT in depth earlier; the short version is that it is still experimental but genuinely useful for compute-intensive loops. Read the full breakdown here.
The new profiling.sampling module (internally called Tachyon, defined in PEP 799) is a statistical profiler that runs at virtually zero overhead and can attach to a live process without code changes or a restart. If you have ever wanted production-safe profiling without the cProfile performance hit, this is it: python -m profiling.sampling myscript.py gets you flamegraphs, heatmaps, and wall-clock breakdowns out of the box.
What to Do Before October
RC1 is the time to act, not to watch. Install it in a separate environment, run your test suite against it, and pay particular attention to any code that does text-mode file I/O without an explicit encoding. If you ship Python packages, build wheels against rc1 now — they are ABI-stable for the final release and you will be ahead of the October rush.
pyenv install 3.15.0rc1
pyenv virtualenv 3.15.0rc1 test-315
pip install -e . # install your package, run your tests
Python 3.15 is a strong release. Lazy imports and frozendict add genuine language-level value. The UTF-8 change is the right call even if it creates migration work. The full release notes and rc1 download are on python.org. October will be here faster than it looks.













