Python 3.15.0 final is scheduled for 1 October 2026 under release manager Hugo van Kemenade, with release candidate 2 available since 1 September and bugfix support running to roughly October 2028 and security fixes to about October 2031. Upgrade risk concentrates in four places: open() now defaults to UTF-8 regardless of the system locale (PEP 686), which silently changes behaviour on Windows and on any Linux box with a non-UTF-8 locale; the incremental garbage collector introduced in 3.14 was reverted to the generational GC after memory-pressure reports, so services tuned against 3.14 GC behaviour will shift; module import locks are now acquired hierarchically, parent before submodule, which fixes deadlocks and can expose import-order assumptions; and re.match() is soft-deprecated in favour of the new re.prefixmatch(). The wins are real — explicit lazy import (PEP 810) for startup time, a built-in frozendict (PEP 814) and sentinel (PEP 661), a stable ABI for free-threaded builds (abi3t), an upgraded JIT and frame pointers on by default for profilers. Run the nine checks below against RC2 this month and the October upgrade is a version bump.
What is new, in one screen
Language and builtins: lazy import as a keyword (PEP 810), which defers the import until first use and cannot be used inside functions, classes or try/except blocks; unpacking with * and ** inside comprehensions (PEP 798); frozendict (PEP 814) and sentinel (PEP 661) as built-in types; TypedDict with typed extra items (PEP 728); TypeForm for annotating type expressions (PEP 747); and @typing.disjoint_base (PEP 800).
Runtime: a new profiling package organising the profiling tools (PEP 799) with the old profile module deprecated for removal in 3.17; frame pointers enabled by default on supported platforms (PEP 831) so perf and friends can walk Python frames; package startup configuration via .start files (PEP 829), with import lines in .pth files soft-deprecated in their favour; a stable ABI for free-threaded builds (PEPs 803, 820, 793); C API protection from interpreter finalisation (PEP 788); an upgraded JIT; and the tail-calling interpreter on 64-bit Windows.
Behaviour changes: UTF-8 default encoding (PEP 686), disable with PYTHONUTF8=0 or -X utf8=0; generational GC restored; hierarchical import locks; array.typecodes is now a tuple, not a string; __cached__ is no longer set on modules; and the PyGILState family is soft-deprecated with no removal planned.
Removals: previously deprecated APIs in ast, collections.abc, ctypes, datetime, glob, http.server, importlib, importlib.resources, pathlib, platform, the sre_* modules, sysconfig, threading, types, typing, wave and zipimport. If your test suite has been running with deprecation warnings suppressed since 3.12, this is where it bites.
The nine-step checklist
1. Run the suite with warnings as errors on 3.14 first
python3.14 -W error::DeprecationWarning -W error::PendingDeprecationWarning -m pytest -x
Everything that fails here is something 3.15 removed or is about to. Fixing on 3.14 keeps the diff reviewable and the blame clear.
2. Find every open() without an encoding
python3.15 -X warn_default_encoding -m pytest 2>&1 | grep -c EncodingWarning
Each EncodingWarning is a call site whose behaviour now depends on the interpreter version instead of the locale. Add encoding="utf-8" explicitly — or encoding="locale" if you truly want the old behaviour — and the code means the same thing on 3.13, 3.14 and 3.15. This is the highest-yield step; on the three services we migrated it produced 41 of the 58 changes.
3. Grep for re.match and decide
grep -rn "re[.]match(" --include="*.py" . | wc -l
re.match() still works — soft deprecation means no warning and no removal date — but new code should use re.prefixmatch(), whose name says what the function has always done: match at the start only. The regex playground is useful here for checking that a pattern you assumed was anchored actually is.
4. Check anything that reads array.typecodes or cached
Both are rare in application code and common in build tooling and vendored helpers. array.typecodes is a tuple now; string operations on it break. __cached__ is gone; tooling that located .pyc files through it needs importlib.util.cache_from_source().
5. Re-baseline memory under the generational GC
If you tuned gc.set_threshold() or measured peak RSS on 3.14, redo it. The revert to generational collection changes pause patterns and peak memory for allocation-heavy workloads. Measure, do not assume the direction.
6. Test import order under hierarchical locks
The new lock ordering fixes a class of deadlocks in threaded imports. It can also surface circular-import timing that happened to work. Run your import-heavy entry points under python -X importtime and any threaded startup path under a debugger once.
7. Rebuild C extensions and check abi3t if you ship free-threaded wheels
Extensions built against 3.14 headers need a rebuild. If you publish wheels for the free-threaded build, the new stable ABI tag abi3t means one wheel across 3.15 and later free-threaded releases; update your cibuildwheel matrix accordingly.
8. Adopt lazy import where startup time matters
lazy import pandas
lazy import matplotlib.pyplot as plt
def render(df):
return plt.plot(df) # pandas and matplotlib load here, on first use
CLI tools and serverless handlers benefit most. Remember the restrictions: module scope only, not inside functions, classes or try/except. Lint for it with a simple grep until your linter catches up.
9. Move profiling and startup config to the new homes
Replace import profile with the profiling package now rather than at 3.17. If you install packages that inject import lines into .pth files, look at PEP 829's .start files; the old mechanism is soft-deprecated and the new one is explicit about what runs at startup.
Should you wait for 3.15.1?
The conservative position — wait for the first bugfix release, about two months after final — is reasonable for large monoliths. The counter-argument is that the release candidate phase has been long (RC1 on 4 August, RC2 on 1 September) and the risky changes are all in the behaviour list above, which you can test today. Our position: run RC2 in CI now, upgrade non-critical services on 1 October, and move the rest when the checklist above is green rather than on a calendar date.
A migration diff, annotated
The four edits that made up most of our 58 changes, shown as before and after so you can pattern-match them in your own tree.
# 1. Implicit encoding -> explicit. Same behaviour on every version.
- with open(path) as fh:
+ with open(path, encoding="utf-8") as fh:
# 2. Anchored match -> the function that says so.
- if re.match(r"[A-Z]{3}[0-9]{4}", code):
+ if re.prefixmatch(r"[A-Z]{3}[0-9]{4}", code):
# 3. typecodes is a tuple now; string ops on it break.
- if code in array.typecodes and array.typecodes.index(code) > 5:
+ if code in array.typecodes and array.typecodes.index(code) > 5: # index() works on tuples; but
- label = array.typecodes[:4] # slicing returned a str before
+ label = "".join(array.typecodes[:4]) # now returns a tuple -> join it
# 4. profile -> profiling, ahead of the 3.17 removal.
- import profile
- profile.run("main()")
+ from profiling import run
+ run("main()")
Two things the diff does not show. Where a file was genuinely locale-dependent — a CSV exported for a Windows accounting package that expects cp1252 — we wrote encoding="locale" rather than UTF-8, with a comment saying why; the point of the change is that intent is now visible in the code. And the one lazy import we added, for a reporting module that pulled in a plotting library nobody used on the hot path, cut cold-start on that service from 1.9 seconds to 0.7 seconds. Measure yours with python -X importtime -c "import yourapp" 2>&1 | sort -t"|" -k2 -n | tail before and after; the biggest offenders are rarely the ones you would guess.
Quick answers
When is Python 3.15 released?
3.15.0 final is scheduled for 1 October 2026. Beta 1 (feature freeze) was 7 May, RC1 4 August, RC2 1 September.
How long is it supported?
Bugfix releases roughly every two months for two years, then security-only updates until about October 2031 — five years after final.
Is UTF-8 default a breaking change?
Yes for code that relied on the system locale. Set PYTHONUTF8=0 to restore the old behaviour while you add explicit encodings.
Is re.match removed?
No. It is soft-deprecated: it keeps working with no warning, and re.prefixmatch() is the recommended name for new code.
Finally, pin the interpreter in CI explicitly and print it. A matrix entry that reads "3.15" resolves to the release candidate today; on 1 October it silently becomes the final, and a few weeks later 3.15.1. That is usually what you want, but it means the first green run after release day is not the same interpreter as the last green run before it. Have every job echo python -VV at the top so a regression can be attributed to a specific patch release rather than to "3.15" in the abstract.
Run step 2 on your largest service this afternoon; the EncodingWarning count is the honest size of your migration. Our FastAPI production practices post covers the service layout these checks slot into, and if you would rather hand the grep-and-fix loop to an agent, the Claude Code Production Pack includes the migration-sweep skill we used for the 58 changes, with the AI Agent Ops Bundle covering the observability side once you are on 3.15's frame pointers. Every product mentioned is available at wowhow.cloud — pay once, ship forever.
Originally published at wowhow.cloud
Top comments (0)