Python 3.15: what actually changes for a backend service

Lire en français

Python 3.15 shipped on 9 October 2026. Most of the What’s New will never reach a running backend service: unpacking in comprehensions is pleasant in review and invisible in production. Four changes do reach it. One of them will break a deploy if you have been careless with encoding=.

UTF-8 is the default text encoding now

PEP 686 makes UTF-8 mode the default on every platform. open() without encoding=, subprocess.run(text=True), and locale.getpreferredencoding(False) all land on utf-8, regardless of the Windows code page or LANG.

The failure mode is asymmetric. A file written by an older Windows service in cp1252 raises UnicodeDecodeError, and you find it in five minutes. A file whose bytes happen to be valid UTF-8 decodes into mojibake and sits in the database for a year. The second one is the reason to care.

Fastest audit: run the test suite with -X warn_default_encoding, which has raised EncodingWarning on implicit locale encoding since 3.10, and fix every hit by passing encoding= explicitly. That is a two-hour job on a mid-sized service, and it makes the upgrade a non-event. PYTHONUTF8=0 still turns UTF-8 mode off, so there is a stopgap if a vendor library misbehaves.

JSON and HTTP are unaffected. They were already UTF-8 or declared their charset. The blast radius is local files and anything a vendor CLI prints in the console code page.

Lazy imports move work out of start-up

PEP 810 adds lazy import x and lazy from x import y as statements you opt into per module. The module body does not execute until the first attribute access.

For a service, that is cold-start budget. Django settings that import every app at module scope, or a CLI that builds the app factory before it parses --help, both pay for imports nobody used. Gunicorn with four workers pays four times. The PEP does not promise a percentage and neither will I: measure with python -X importtime -c "import app" before and after.

The trap is import order. A module with import-time side effects (registering into a global registry, reading an environment variable into a constant) now does it later, possibly on a request thread. Lazy imports are safe for pure definitions and dangerous for anything that registers itself.

frozendict for config, sentinel for defaults

PEP 814 adds frozendict to builtins. It is an immutable mapping, so a module-level defaults object can be handed to callers without a defensive copy:

DEFAULTS = frozendict({"timeout": 5, "retries": 2})
DEFAULTS["timeout"] = 10   # TypeError

That replaces the types.MappingProxyType habit with something that has a real repr and a constructor taking any mapping. If you pass config defaults around, this is the cheapest correctness win in the release.

PEP 661 covers the sibling problem, the None-as-default ambiguity. A module-level MISSING = sentinel("MISSING") is a unique object whose repr says what it is, so a log line reads MISSING instead of <object object at 0x7f3c9a2e1d80>. Same pattern you already write by hand, one import less.

Neither is worth a refactor on its own. They are worth adopting as you touch the code.

Tachyon samples instead of instrumenting

PEP 799 adds a profiling package to the standard library. The interesting half is Tachyon, a statistical sampler: it reads the stack at high frequency instead of hooking every call and return.

cProfile’s overhead scales with call count, which is why a profile of a call-heavy service is sometimes a profile of the profiler. A sampler’s overhead is roughly flat, and time blocked on a socket or inside a C extension shows up as samples in that frame rather than vanishing. For an endpoint waiting 300ms on Postgres, that is the whole answer.

PEP 831 helps here: frame pointers are now enabled by default in the builds, so native stack walking is cheap for anything that has to unwind past the interpreter.

Sampling has one honest cost. A function that runs for less than the sampling interval may never appear, so keep the deterministic profiler in the same package for those.

When not to do this

I would not migrate a service in October 2026 for lazy imports or frozendict. If your process starts in 400ms and your config defaults are already copied defensively, the work buys you nothing.

The encoding change is not optional. It arrives with the interpreter whether you plan for it or not.

One caveat that has nothing to do with the What’s New: an extension module not rebuilt against the 3.15 ABI fails at import, not at upgrade. Check the dependency tree before the weekend.

← All posts