TL;DR

Python 3.15 hits its final release on October 1, 2026, and the beta is already installable. The headline additions are a lazy import statement that defers module loading until first use, a real frozendict builtin, */** unpacking inside comprehensions, and a statistical sampling profiler (Tachyon) that can attach to a running process at up to a million samples per second. The JIT gets a reported 8–13% geometric-mean speedup, and the garbage collector quietly reverts the 3.14 incremental change that had been eating memory in production. None of the new syntax is on by default, so upgrading won’t break your code, but a few of these features are worth rewriting for.

I pulled the 3.15 beta changelog and ran every new snippet against my own 3.12.3 install to see exactly what changes and what stays a workaround. The contrasts below are the real output from both.

Why I care about a point release

Most Python releases give you three headline features and forty stdlib footnotes you’ll never notice. I usually skim the what’s-new page, bump my CI matrix, and move on. Python 3.15 is the first one in a while where I read the changelog twice, because two of the features fix problems I’ve been hand-coding around for years: slow startup from eager imports, and the absence of a hashable read-only dict.

Instead of summarizing the doc, I did what I’d tell anyone to do before an upgrade. I opened a REPL on my current interpreter, typed each new snippet, and captured what happened. Here’s my starting point:

$ python3 --version
Python 3.12.3

Everything marked “3.12” below is genuine output from that shell. Everything marked “3.15” is from the official what’s-new document, since I’m not going to fake a beta run and call it hands-on.

Lazy imports finally land (PEP 810)

This is the one I’d been waiting for. Big applications pay a startup tax for imports they might never touch on a given code path. A CLI that imports xml.etree.ElementTree at the top of a module pays for it even when the user runs --help.

I measured that tax on my machine with -X importtime:

$ python3 -X importtime -c "import json, argparse, decimal, xml.etree.ElementTree"
...
import time:      1763 |       7397 | xml.etree.ElementTree

That’s 7.4 milliseconds of cumulative import time for a single tree module I might not use. Multiply across a dependency graph and startup creeps past a second for tools that should feel instant.

The old fix was to shove imports inside functions, which works but scatters your dependencies and confuses every linter and type checker. Python 3.15 gives you a real statement instead:

lazy import json
lazy from pathlib import Path

print("Starting up...")   # neither json nor pathlib is loaded yet
data = json.loads('{"key": "value"}')   # json is imported right here, on first use

On 3.12 that syntax doesn’t exist. My REPL is blunt about it:

--- lazy import keyword? ---
SyntaxError: invalid syntax

Two things kept me from getting over-excited. First, lazy is a soft keyword and the feature is opt-in per import; it won’t silently change how existing code loads. Second, there’s a global switch (-X lazy_imports or the PYTHON_LAZY_IMPORTS env var) plus sys.set_lazy_imports() for turning it on process-wide, but PEP 810 restricts lazy imports to module scope, with no star imports and no __future__ imports. That’s a sane boundary. If you maintain a CLI or a plugin host, this is the single feature most likely to earn its keep. If you’re writing a web service that imports everything at boot anyway, you’ll feel it less.

frozendict: a real immutable mapping (PEP 814)

For years the “read-only dict” answer in Python was types.MappingProxyType. It’s a live view onto another dict, and it isn’t hashable, so you can’t use it as a dict key or drop it in a set. Here’s the wall I hit on 3.12:

--- current read-only dict idiom (MappingProxyType) ---
proxy: {'x': 1}
hashable? no -> unhashable type: 'dict'

And plain frozendict simply doesn’t exist yet:

--- frozendict builtin? ---
NameError: name 'frozendict' is not defined

Python 3.15 promotes frozendict to a builtin (PEP 814). It’s immutable and, as long as its keys and values are hashable, it’s hashable too. The what’s-new doc shows the shape:

>>> a = frozendict(x=1, y=2)
>>> a['z'] = 3
TypeError: 'frozendict' object does not support item assignment
>>> b = frozendict(y=2, x=1)
>>> hash(a) == hash(b)
True
>>> a == b
True

Note the last two lines: equality and hashing ignore insertion order, which is what you want for a value type. Several stdlib modules learned to speak it too, including json, pickle, copy, and pprint, and json.loads gains an array_hook so you can decode a payload straight into nested frozendict plus tuple and get a genuinely immutable structure back. If you’ve ever built a cache keyed on a config dict and reached for frozenset(d.items()), this replaces that hack.

Unpacking inside comprehensions (PEP 798)

This one is small, but it fixes a real annoyance. Per PEP 798 you can now splat with * and ** inside list, set, and dict comprehensions and generator expressions. Flattening a list of lists stops needing the double-for incantation that everyone has to re-read twice.

My 3.12 shell rejects it outright:

--- unpacking in comprehension? ---
SyntaxError: iterable unpacking cannot be used in comprehension

In 3.15 the same line runs, per the doc:

>>> lists = [[1, 2], [3, 4], [5]]
>>> [*L for L in lists]
[1, 2, 3, 4, 5]

>>> dicts = [{'a': 1}, {'b': 2}, {'a': 3}]
>>> {**d for d in dicts}
{'a': 3, 'b': 2}

It reads the way you’d say it out loud: for each list, spread its items. I expect this to quietly delete a lot of itertools.chain(*lists) calls from my code.

The Tachyon sampling profiler (PEP 799)

If you’ve only ever reached for cProfile, you know its problem: it instruments every call, which slows the target down enough to distort the very numbers you’re chasing, and you can’t point it at a process that’s already running and misbehaving in production.

Python 3.15 reorganizes profiling into a dedicated profiling package (PEP 799). profiling.tracing is the deterministic profiler you already know (cProfile stays as an alias), and profiling.sampling is new. It’s a statistical sampler, nicknamed Tachyon, that pokes the interpreter at intervals instead of wrapping every call. The what’s-new doc puts the ceiling at up to 1,000,000 Hz, and because it attaches to a live process, you can profile something that’s stuck without restarting it.

On 3.12 the module isn’t there at all:

--- sampling profiler module (profiling.sampling)? ---
ModuleNotFoundError: No module named 'profiling'

What sold me reading the changelog was the output modes. You get --mode wall, --mode cpu, and --mode gil, plus exporters for flamegraphs, Firefox-compatible Gecko traces, a heatmap, and a live TUI. There’s an --async-aware flag for coroutine-heavy code and --opcodes for bytecode-level detail. PEP 831 also turns on frame pointers by default in the official builds, which is the unglamorous plumbing that makes native stack unwinding reliable for exactly this kind of tool. For anyone who has tried to profile a wedged asyncio service, “attach to the running PID and get a flamegraph” is the whole pitch.

Speed: the JIT and the GC U-turn

The two performance changes in 3.15 pull in opposite directions.

The JIT compiler, still experimental and off unless you build with it, gets a real upgrade in 3.15: a new tracing front end and register allocation for the generated machine code. InfoWorld puts the win at a reported 8–13% geometric-mean improvement over standard CPython, depending on platform and workload. I’d treat that as “reported until you benchmark your own code,” because JIT gains are famously workload-shaped, but the direction is right.

The garbage collector is the quieter and more interesting one. Python 3.14 shipped an incremental GC. Then enough people reported higher process memory in production that the core team reverted it. Python 3.15 goes back to the generational collector from 3.13 and earlier. It’s a rare public U-turn, and I read it as a healthy sign: a change went out, real workloads pushed back with data, and the default flipped back. If you saw memory creep after moving to 3.14, this is your fix.

Oct 1, 2026
3.15.0 final release
8–13%
Reported JIT geomean speedup
1,000,000 Hz
Tachyon max sampling rate

Quieter wins worth a mention

A few more that didn’t need their own section but will show up in real code.

The new sentinel builtin (PEP 661) retires a pattern everyone has hand-rolled: a unique “no value here” object that distinguishes “argument omitted” from “argument was None.” Here’s the boilerplate I still write today, running fine on 3.12:

--- hand-rolled sentinel idiom (current) ---
MISSING repr: <MISSING> | identity kept: True

The 3.15 builtin produces a unique, picklable, copy-stable value with a clean repr, so that whole singleton class goes away.

On the typing side, TypeForm (PEP 747) lets a function annotate that it takes a type expression as a value, which is what libraries like cast() and validation frameworks have wanted forever. TypedDict also picks up closed and extra_items (PEP 728) so you can finally say “no keys beyond these” or “extra keys, but they must be int.” If you lint your code, pair this with a modern checker; I compared the current options in Pyrefly vs mypy vs ty.

The no-GIL build also gets its own stable ABI, abi3t (PEP 803), so C extensions can ship one wheel that keeps working across future free-threaded releases instead of recompiling every version. It’s boring infrastructure, and it’s the part that decides whether no-GIL Python is actually usable at scale. I dug into the runtime side of that story in Python 3.14 free-threading, and 3.15 is the packaging half of the same effort.

One default worth knowing about: Python now uses UTF-8 for I/O without an explicit encoding, independent of the system locale. You can opt out with PYTHONUTF8=0, but the long-running “why is this file mojibake on Windows” class of bug is finally addressed where it should be.

How the releases stack up

Here’s the short version of where each recent feature lives, so you know what you’re actually getting when you bump the interpreter.

FeatureVersionDefault?Who benefits most
Free-threading (no-GIL) build3.13 experimental → 3.14 supportedOff (separate build)CPU-bound multithreaded code
Incremental GC3.14 (added) → 3.15 (reverted)Back to generationalAnyone who saw memory creep
lazy import statement3.15Off (opt-in per import)CLIs, plugin hosts, slow-start apps
frozendict builtin3.15Always availableCache keys, config values
Unpacking in comprehensions3.15Always availableEveryone flattening iterables
Tachyon sampling profiler3.15Available in stdlibProfiling live/production processes
Upgraded JIT3.15Off (build flag)CPU-bound hot loops

How to try Python 3.15 today

You don’t have to touch your system Python to kick the tires. The cleanest path is a version manager that installs an isolated build. With uv, grabbing the beta and opening a REPL is two commands:

uv python install 3.15
uv run --python 3.15 python

From there, run the snippets above yourself: try lazy import json, build a frozendict, and flatten a list with [*L for L in lists]. If you’d rather stay on a package-manager-native flow, pyenv install 3.15.0b3 does the same job. Either way, point one project’s CI at 3.15 now so you find the deprecation warnings before the October final lands. The full list of what’s changing across the stdlib is long, and if you’re modernizing a codebase it pairs well with the rest of the toolchain I walk through in the modern Python tooling guide.

One caution: this is a beta, and the JIT and free-threading builds are still experimental. Don’t ship production on it. Do read the deprecations, because 3.15 soft-deprecates re.match() in favor of the clearer re.prefixmatch() and schedules the old profile module for removal in 3.17.

FAQ

When is Python 3.15 released?

The final 3.15.0 release is scheduled for October 1, 2026. Beta 1 shipped on May 7, 2026, and the feature freeze has already passed, so the feature set is locked even though bug fixes continue.

Is Python 3.15 faster than 3.14?

For most code, startup is the visible win, thanks to lazy imports deferring module loading. The experimental JIT reports an 8–13% geometric-mean improvement over standard CPython, but it’s off unless you build with it, and gains depend heavily on your workload. The GC revert also helps if you saw memory growth on 3.14.

What is the new profiler in Python 3.15?

It’s Tachyon, exposed as profiling.sampling. Unlike cProfile, which instruments every function call, Tachyon takes statistical samples at up to a million hertz and can attach to a process that’s already running, then export flamegraphs, Gecko traces, or a live terminal view.

Are lazy imports enabled by default in Python 3.15?

No. lazy import is opt-in per statement, and the process-wide switches (-X lazy_imports, PYTHON_LAZY_IMPORTS, sys.set_lazy_imports()) are also opt-in. Existing code keeps loading modules eagerly unless you ask otherwise, so upgrading won’t change your import behavior.

Should I upgrade to Python 3.15 now?

Test on it, don’t deploy on it. Run a project’s CI against the beta to surface deprecation warnings early, but wait for the October final release before shipping production, especially if you depend on the JIT or free-threading builds, which remain experimental.

Sources

The bottom line

Python 3.15 is a quietly practical release. Lazy imports and frozendict alone justify reading the changelog, the profiler reorg finally makes production profiling first-class, and the GC revert shows the core team is willing to undo a shipped default when the data says so. Nothing here forces a rewrite, and nothing here breaks on upgrade. But I’ve already got two projects where lazy import will shave real startup time, and that’s more than I can say for most point releases. Put a CI job on the beta now, and enjoy the October release when it arrives.