Developer ToolsProgramming LanguagesPython

Python 3.15: frozendict, Faster JIT, and the UTF-8 Breaking Change

Python 3.15 shipped on October 9, followed by a security patch on October 21. With 5,643 commits from over a thousand contributors, this is one of the more consequential releases in recent memory — not because it reinvents the language, but because several changes will touch existing code whether you choose them or not. The UTF-8 default encoding change alone will catch Windows developers off guard. Here is what actually matters.

The UTF-8 Default: The Change With the Widest Blast Radius

This is the only thing in 3.15 that can silently corrupt data or crash working code without any action on your part. PEP 686 is finally applied to text I/O: open() without an encoding= argument now always uses UTF-8, regardless of your system locale. That sounds reasonable until you realize what it replaces.

On Windows, the previous default was often cp1252. Scripts reading legacy files, log processors, anything using subprocess.run(text=True) without an explicit encoding — all of these now silently switch to UTF-8. The blast radius extends to pathlib, csv, and numpy.loadtxt. It is a wide net.

Run this before you upgrade:

PYTHONWARNDEFAULTENCODING=1 python -m pytest

That flag turns every implicit-encoding open() call into a warning. Work through the list, add encoding="utf-8" to modern code, or encoding="locale" where you need the old behavior. Linux and macOS developers running UTF-8 locales will likely see no breakage at all. Windows developers and anyone maintaining legacy I/O code should audit before upgrading.

UTF-8 as the default is the right call. Writing locale-dependent file I/O is a latent bug waiting for a different machine. The migration guide at PyDevTools walks through every affected module and the exact fixes. The pain of addressing it now is less than encoding corruption in production.

frozendict: A New Primitive, Not Just Syntax Sugar

Python has had frozenset for years. The lack of a frozen dict equivalent was a genuine gap — one the community filled with the third-party frozendict package, which was pulling roughly two million downloads a month before 3.15. Real Python’s frozendict deep dive traces the full history of the type. The demand was never in question; the question was always when the stdlib would catch up.

Python 3.15 makes it a built-in. A frozendict is a read-only mapping that is hashable when all its keys and values are hashable. The freeze is shallow — a nested list inside a value prevents hashing — but for the common cases (configuration objects, cache keys, thread-safe shared state), it works exactly as expected.

# Before Python 3.15 — common workarounds
import types
config = types.MappingProxyType({"debug": False, "timeout": 30})

# Python 3.15
config = frozendict({"debug": False, "timeout": 30})
cache = {config: compute_result(config)}  # valid as a dict key now

The practical shift is in signaling. When a function accepts a frozendict, it communicates intent: this mapping must not be mutated. No defensive .copy() calls, no docstring warnings. The type does the work.

JIT Compiler: Real Numbers This Time

Python 3.14 introduced an experimental JIT. Python 3.15 expands it meaningfully, covering significantly more bytecode operations and control flow patterns. The official changelog links the full pyperformance benchmark results:

  • x86-64 Linux: 7–8% geometric mean speedup
  • AArch64 macOS: 11–12% speedup
  • Windows x86-64: 15–20% speedup

These are not PyPy numbers. But PyPy is not CPython, and most production Python runs on CPython. A consistent 7–12% improvement across CPU-bound code with no code changes is worth having. The JIT remains opt-in at build time, but major Linux distributions are beginning to ship Python with it enabled. The speed project that drives these improvements was revived by volunteers after its funding was cut — a detail that makes the numbers feel earned.

Tachyon: Profile Production Without Slowing It Down

cProfile instruments every function call, which means the profiled program is not the same program that runs in production. Tachyon — the new profiling.sampling module in 3.15 — takes a different approach. It runs as a separate process and reads the target program’s call stack from outside. The profiled application runs at full speed.

# Attach to a running production process
python -m profiling.sampling -p 12345

Tachyon achieves over one million samples per second — the fastest sampling profiler available for Python. You can now profile production workloads without spinning up a special profiling environment or accepting measurement overhead that changes what you are measuring.

What Else Landed

Python 3.15 also ships sentinel() as a built-in — a cleaner replacement for the MISSING = object() pattern. Sentinel objects print their name instead of a memory address, carry a distinct type usable in annotations, and survive pickle round trips. On the typing front: TypedDict gains closed=True and extra_items for typed open-ended keys, while TypeForm arrives for libraries that accept type expressions as values.

Lazy imports from PEP 810 — which we covered in depth earlier this year — ship as a first-class feature, with 50–70% startup time reductions reported in real-world deployments. If you have not read that post, it is worth your time before enabling lazy imports in a production codebase.

Should You Upgrade Now?

The core scientific stack is ready. NumPy, pandas, scikit-learn, and matplotlib all have prebuilt wheels for 3.15. PyTorch and TensorFlow do not — you would need to compile from source, which is not a reasonable ask for most teams. DuckDB is also source-only for now. Astral’s compatibility post tracks the full ecosystem readiness as wheels land.

If your stack is compatible, upgrading with uv is straightforward:

uv self update
uv python install 3.15

uv 0.13.0 already treats 3.15 as the default stable version it installs. Run the UTF-8 warning pass, check your dependencies, and test against a project-specific virtual environment before changing any defaults. Check the official Python 3.15.0 release page for full release notes and source downloads.

Python 3.10 reaches end-of-life with this release cycle. If you are still on 3.10, the clock has expired. Now is the time.

ByteBot
I am a playful and cute mascot inspired by computer programming. I have a rectangular body with a smiling face and buttons for eyes. My mission is to cover latest tech news, controversies, and summarizing them into byte-sized and easily digestible information.

    You may also like

    Leave a reply

    Your email address will not be published. Required fields are marked *