The Python 3.15 release candidate is out, and it is the last one planned. Python 3.15.0rc3 shipped on October 2, and the python.org release page says the final 3.15.0 build, postponed by a week, is now scheduled for October 9. That leaves about six days to find out whether your code survives the upgrade before everyone else does.

Release candidates exist for this. A bug reported now can still be fixed in the final build, while one that surfaces after October 9 waits for a point release. Library maintainers benefit most, since a test run against rc3 is the early warning that downstream users would otherwise deliver, less politely, a week from now.

This guide covers one afternoon of work: building the interpreter, wiring it into CI, flushing out the encoding change, trying lazy imports, profiling with the new sampler, and scanning for the standard-library changes most likely to break working code. The facts come from the Python 3.15.0rc3 release page and the in-progress What’s New in Python 3.15 document, both of which can still change before the final.

What Is New in 3.15 at a Glance

FeaturePEPWhy it matters
Explicit lazy imports810lazy import json defers loading until the name is first used, which helps startup time
frozendict built-in814Immutable, hashable mapping that is not a subclass of dict
sentinel built-in661Unique marker values with a concise repr
Unpacking in comprehensions798[*L for L in lists] and {**d for d in dicts} now work
UTF-8 as the default encoding686Text I/O without an explicit encoding no longer follows the locale
Tachyon sampling profiler799Samples up to 1,000,000 Hz and can attach to a running process
Frame pointers on by default831Native build flags change; unwinding and profiling improve
Stable ABI for free-threaded builds803New abi3t target for extension authors
JIT upgradenoneStill experimental; see the benchmark note near the end

Step 1: Build or Install the Release Candidate

Grab the installer or source tarball from the release page. Windows and macOS users can install it alongside their current Python. The page notes that the official Windows 64-bit binaries use the tail-calling interpreter and that the official macOS binaries install free-threading support by default, so both builds behave a little differently from what you may be used to.

Server rack with rows of green status lights

On Linux, building from source is the cleanest route. The altinstall target keeps the new interpreter from replacing your system python3.

tar xf Python-3.15.0rc3.tar.xz
cd Python-3.15.0rc3
./configure --enable-optimizations --prefix=$HOME/.local/python-3.15rc
make -j4
make altinstall
$HOME/.local/python-3.15rc/bin/python3.15 -VV

Build prerequisites such as OpenSSL, libffi and zlib headers vary by distribution, so install those first if configure complains. Then create a throwaway environment and install your project with its test extras.

$HOME/.local/python-3.15rc/bin/python3.15 -m venv .venv-315
. .venv-315/bin/activate
python -m pip install -e ".[test]"

Compiled dependencies are the usual stumbling block. A package without a cp315 wheel falls back to a source build and needs a compiler, which is where many first runs stall. The release page states that there are no ABI changes from rc3 onward, so extension maintainers can treat this as the moment to rehearse their own builds.

One platform warning matters if you develop on a Mac. According to the release page, macOS 27.0 may cause IDLE and other tkinter applications to hang when they open dialogs, and the recommendation is to defer the macOS 27.0 upgrade until a fix is available. Updates are tracked in CPython issue 158053.

Step 2: Add 3.15 to Your CI Matrix

One extra line in continuous integration turns a one-off experiment into a standing check. With GitHub Actions, the setup-python action needs its allow-prereleases input switched on to resolve a version that has no general-availability build yet.

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        python-version: ["3.13", "3.14", "3.15"]
    steps:
      # Use the action versions your pipeline already pins
      - uses: actions/checkout@v5
      - uses: actions/setup-python@v6
        with:
          python-version: ${{ matrix.python-version }}
          allow-prereleases: true
      - run: python -m pip install -e ".[test]"
      - run: python -X dev -m pytest -W error::DeprecationWarning

Setting fail-fast to false keeps a 3.15 failure from cancelling the stable jobs. Consider marking the 3.15 job as allowed to fail for the first week. The final step runs in dev mode, which switches on extra runtime checks, and promotes deprecation warnings to errors so they stop being background noise. If a dependency you cannot control produces warnings, narrow the filter to your own package instead of abandoning the check.

Tox users can add py315 to the environment list. Nox users can add "3.15" to a session’s python list.

Step 3: Find Code That Depends on the Locale Encoding

The change most likely to alter behavior silently is PEP 686. In 3.15, text I/O that is given no encoding uses UTF-8 regardless of the machine’s locale. A call such as open("notes.txt") used to follow the locale, so a Windows laptop and a Linux server could read the same file differently. Now they agree. The catch is any file that was never UTF-8 to begin with.

The tooling to find exposed spots predates 3.15. Run your test suite on an older interpreter with the encoding warning enabled, and treat the warning as an error.

python3.14 -X warn_default_encoding -W error::EncodingWarning -m pytest

Each hit gets one of two fixes. If the data is UTF-8, say so. If it follows the machine, as with a legacy spreadsheet export, name the real codec or pass encoding="locale", which has been available since Python 3.10.

# Before: behavior depended on the machine locale
with open("export.csv") as f:
    rows = f.read()

# After: the file really is Windows-1252, so say so
with open("export.csv", encoding="cp1252", newline="") as f:
    rows = f.read()

Teams that cannot finish the cleanup in time have an escape hatch. Setting PYTHONUTF8=0 or running with -X utf8=0 opts out. Treat it as a bridge, not a destination.

Step 4: Try Lazy Imports on Your Slowest Startup Path

Start with a measurement, because lazy imports only pay off where import time is real. Python’s importtime option prints the cost of every import to standard error.

Stopwatch resting beside an open laptop
python3.15 -X importtime -c "import myapp" 2> imports.txt
sort -t"|" -k2 -n imports.txt | tail -15

The second column is cumulative microseconds, so the heaviest entries at the bottom are your candidates. PEP 810 adds lazy as a soft keyword. A lazy statement binds the name right away but defers loading the module until the name is first used.

lazy import json
lazy from pathlib import Path

def load(path):
    return json.loads(Path(path).read_text())
  • Lazy imports are allowed only at module scope. Inside a function, a class body, or a try block they raise SyntaxError.
  • lazy from module import * and lazy from __future__ import ... also raise SyntaxError.
  • Loading errors, such as a missing module, surface at first use, and the traceback includes the original import statement.

For testing at scale you can flip the default without touching source. The -X lazy_imports option and the PYTHON_LAZY_IMPORTS environment variable accept all or normal, and sys.set_lazy_imports() does the same at runtime. A callable passed to sys.set_lazy_imports_filter() receives the importing module, the imported module and the from-list, then returns True to allow laziness. A module can also list names in __lazy_modules__ so that ordinary import statements for them become lazy.

PYTHON_LAZY_IMPORTS=all python3.15 -m pytest

Run the suite once in that mode and expect some noise. Import-time side effects such as plugin registration, monkeypatching or signal handlers move to first use, and code that relied on their order will notice. Whether the failure is a bug in your code or a quirk of your test is a judgment call, so read each one before dismissing it.

Step 5: Profile With Tachyon, the New Sampling Profiler

PEP 799 reorganizes profiling into a package. The module profiling.tracing holds the deterministic tracer relocated from cProfile, which stays as an alias, and profiling.sampling holds Tachyon. The old profile module is deprecated and scheduled for removal in Python 3.17. Tachyon samples at up to 1,000,000 Hz with low overhead and needs neither code changes nor restarts.

# Run a script under the sampler and write a flame graph
python -m profiling.sampling run --flamegraph -o profile.html script.py

# Attach to a running process for 30 seconds
python -m profiling.sampling attach --flamegraph -d 30 -o profile.html 12345

# Raise the sampling rate (the default is 1khz)
python -m profiling.sampling run -r 20khz -d 60 script.py

# Print a one-shot stack dump of a running process
python -m profiling.sampling dump 12345

The --mode flag chooses what the samples measure: wall (the default), cpu, gil or exception. Output options include pstats tables, collapsed stacks, a self-contained HTML flame graph, Firefox Profiler JSON through --gecko, and per-file heatmaps. A --live option gives a top-style terminal view, though it needs curses and therefore works only on Unix-like systems.

One technique suits this exercise well. Record a baseline as a binary profile, change your code, then compare a second run against it. The differential flame graph shows whether the hot spots moved.

python -m profiling.sampling run --binary -o baseline.bin script.py
python -m profiling.sampling run --diff-flamegraph baseline.bin -o diff.html script.py

Step 6: Scan for Changes That Can Break Working Code

Most of the upgrade will be uneventful. The What’s New document does list a handful of behavior changes that deserve a search of your codebase.

AreaChangeWhat to check
importlib.metadataA missing metadata file now raises MetadataNotFound instead of returning an empty PackageMetadataCode that tests metadata for emptiness
arrayarray.typecodes changed from a str to a tupleString operations on typecodes
argparsesuggest_on_error now defaults to TrueTests that compare error output
rere.match() and Pattern.match() are soft deprecated in favor of prefixmatch()Linters and style checks
frozendictInherits from object, not dictisinstance(x, dict) checks; use (dict, frozendict) or collections.abc.Mapping
Import system.pth import lines are silently deprecated; __cached__ is no longer set or consulted; __loader__ and __package__ are deprecatedSite customizations and custom importers
mimetypesapplication/x-texinfo renamed application/texinfo; .ai maps to application/pdfUpload filters and allow-lists
emailGenerators raise an error when a non-ASCII address cannot be flattenedMail code that handles international addresses
gcThe incremental collector from 3.14.0 through 3.14.4 was reverted; 3.15 uses the 3.13 generational collectorMemory and latency numbers tuned on 3.14

For Extension Authors

Maintainers of compiled packages have more homework, and most of it concerns the C API and build flags.

Developer hands on a keyboard with a second monitor of blurred code
  • PEP 803 adds a stable ABI for free-threaded builds, called abi3t. Adopting it means using the PEP 697 APIs instead of embedding PyObject in an instance struct, plus the PyModExport_* hook from PEP 793 and the PySlot structure from PEP 820 in place of PyInit_.
  • Build tools including meson-python, scikit-build-core and Maturin can target abi3t, or you can set Py_TARGET_ABI3T. Extensions that cannot move yet should keep building abi3 and cp315t separately.
  • PEP 831 compiles CPython with frame pointers by default, so any build step that compiles native code should preserve those flags. One component without them can break stack unwinding for profilers.
  • PyGILState_Ensure() and PyGILState_Release() are soft deprecated. They will not be removed, and PEP 788 adds finalization-safe replacements.

Common Questions

Should I run the release candidate in production?

No. A release candidate is a rehearsal. Test now, then upgrade after the final build, which is scheduled for October 9.

Can the final build still differ from rc3?

In principle, yes, which is why this guide leans on the release page and the What’s New text as published with rc3. The release page does state that there are no ABI changes from rc3 onward.

Is the new JIT worth switching on?

It is still labeled experimental. The release page cites a 7 to 8 percent geometric-mean improvement over the standard interpreter on x86-64 Linux, and 11 to 12 percent on AArch64 macOS against the tail-calling interpreter. A geometric mean says little about any single workload, so benchmark your own.

Where do I report a problem?

On the CPython issue tracker on GitHub. Include the exact build, which python3.15 -VV prints, and a minimal reproduction.

Sources