Reference · Python versions · Preview
Preview: Python 3.15
Python 3.15 was released in October 2026. This course is built on Python 3.14, so everything here is a preview: what changes when you move up, how to try it today with Pixi, and what to watch for when you migrate.
- Explain each headline Python 3.15 feature and when you would use it
- Run 3.15 code locally with
pixi execor a dedicatedpy315Pixi environment - Identify the behavior changes that can break a 3.14 codebase (UTF-8 default, GC, deprecations)
- Decide when a project is ready to move its baseline from 3.14 to 3.15
How to try it with Pixi
You don't need to change your project to experiment. pixi exec creates a temporary, cached
environment from a spec and runs a command in it:
# a throwaway 3.15 REPL
pixi exec --spec "python=3.15" python
# run one file with 3.15
pixi exec --spec "python=3.15" python lazy_demo.py
# check the version
pixi exec --spec "python=3.15" python -VV
To test a project against 3.15 alongside its 3.14 default, add a feature and an
environment to pixi.toml. The default environment keeps Python 3.14; the
py315 environment uses 3.15.
[dependencies] # shared by every environment; no python pin here
pytest = "*"
[feature.py314.dependencies]
python = "3.14.*"
[feature.py315.dependencies]
python = "3.15.*"
[environments]
default = ["py314"] # the course baseline
py315 = ["py315"] # the preview
pixi install -e py315 # solve and install the 3.15 environment
pixi run -e py315 python -VV # Python 3.15.x
pixi run -e py315 pytest # your test suite on 3.15
pixi shell -e py315 # a shell with 3.15 active
Explicit lazy imports (PEP 810)
A normal import runs the whole module right away. Large CLIs often import dozens of heavy modules
they never use in a given run, so --help takes a second. The new lazy soft keyword binds
the name immediately but defers the actual import until the name is first used.
Before (3.14): import inside functions
def export(rows):
import json # hidden dependency
return json.dumps(rows)After (3.15): lazy at module level
lazy import json
def export(rows):
return json.dumps(rows)lazyis allowed only at module scope: not inside functions, classes, ortry/exceptblocks (those raiseSyntaxError).lazy from x import *is not allowed.- Import errors surface at first use, not at the
lazy importline. Keep that in mind when reading tracebacks. - Measure the payoff with
python -X importtime. M39 covers startup profiling.
frozendict built-in (PEP 814)
Python had frozenset but no immutable mapping in the builtins. frozendict is a
read-only, hashable dict: it can be a dict key, a set member, a safe default value, or a constant configuration
table that nobody can mutate by accident.
Before (3.14):
from types import MappingProxyType
DEFAULTS = MappingProxyType({"retries": 3})
# read-only view, but not hashableAfter (3.15):
DEFAULTS = frozendict(retries=3)
cache[DEFAULTS] = "ok" # hashableTaught alongside immutability in M18 Functional Python.
sentinel built-in (PEP 661)
A sentinel is a unique marker value meaning "nothing was passed", used when None is
itself a valid argument. The old idiom _MISSING = object() has an ugly repr
(<object object at 0x...>) and does not survive copying or pickling well.
Before (3.14):
_MISSING = object()
def get(key, default=_MISSING): ...After (3.15):
MISSING = sentinel("MISSING")
def get(key, default=MISSING): ...UTF-8 is the default encoding (PEP 686)
Until now, open(path) without encoding= used the locale encoding, which is
UTF-8 on macOS and Linux but often cp1252 on Windows. The same script could read a file correctly on one machine
and produce mojibake (or UnicodeDecodeError) on another. In 3.15 UTF-8 mode is on by
default, so text I/O defaults to UTF-8 everywhere.
Unpacking in comprehensions (PEP 798)
You can now use * and ** directly in a comprehension's output expression to flatten one
level. It reads exactly like the unpacking you already use in list and dict displays.
Before (3.14):
flat = [x for xs in lists for x in xs]
merged = {k: v for d in dicts for k, v in d.items()}After (3.15):
flat = [*xs for xs in lists]
merged = {**d for d in dicts}The profiling package (PEP 799)
Profilers get a single home. profiling.tracing is the deterministic profiler you know as
cProfile (which remains as an alias). profiling.sampling is new: the "Tachyon" sampling
profiler, which periodically records the stack of a running process from the outside. Sampling has almost no
overhead, so you can point it at a production process, and it can produce flame graphs or a live,
top-like view.
# deterministic, function-level counts (formerly cProfile)
pixi run -e py315 python -m profiling.tracing -s cumtime app.py
# sample a script and write an interactive flame graph
pixi run -e py315 python -m profiling.sampling run --flamegraph -o profile.html app.py
# attach to a running process and watch it live
pixi run -e py315 python -m profiling.sampling attach --live 12345
The old pure-Python profile module is deprecated (see migration).
M39 teaches profiling workflow on 3.14 with cProfile.
math.integer (PEP 791)
The integer-only functions of math get their own module: math.integer holds
gcd, lcm, isqrt, comb, perm, and
factorial. Keeping them separate from the floating-point functions makes it clear which results
are exact. The old names in math continue to work.
Smaller library improvements
TOML 1.1 in tomllib
tomllib now parses TOML 1.1: inline tables may span several lines and end with a trailing comma, the
\e and \xHH escapes are allowed, and the seconds in times are optional.
Counter symmetric difference
Counter gains ^: for each key, the absolute difference of the counts.
asyncio.TaskGroup.cancel()
A task group can now be cancelled as a whole, which stops every task in it and exits the
async with block cleanly. Before, you had to raise a custom exception and catch it outside.
Better "did you mean" suggestions
AttributeError suggestions now know common names from other languages, so
[].push(1) suggests .append.
>>> [].push(1)
AttributeError: 'list' object has no attribute 'push'. Did you mean '.append'?
Typing
| PEP | Feature | What it lets you say |
|---|---|---|
| 728 | TypedDict(closed=True), extra_items=T | "This dict has exactly these keys" or "extra keys are allowed, with values of type T". |
| 747 | typing.TypeForm | A parameter that accepts a type expression such as int | None, not just a class. |
| 800 | Disjoint bases (@typing.disjoint_base) | Two classes can never have a common subclass, which lets checkers narrow more precisely. |
Typing on 3.14 is taught in M16. Type checkers adopt new PEPs on their own schedule; check your mypy version's release notes.
Interpreter and performance
Upgraded JIT
The copy-and-patch JIT introduced experimentally in 3.13/3.14 is
significantly upgraded and gives real speedups on many benchmarks. Benchmark your own workload with and without
PYTHON_JIT=1.
Frame pointers by default (PEP 831)
CPython is built with frame pointers, so system profilers (perf, eBPF tools, Instruments) can walk Python and C stacks cheaply and accurately.
Generational GC restored
3.14's incremental collector is reverted in favor of the classic generational one after it caused memory growth in some workloads.
pixi exec --spec "python=3.15" python -c "import sys; print(sys._jit.is_available())"
PYTHON_JIT=1 pixi run -e py315 python bench.py
Migration notes: 3.14 → 3.15
| Change | Risk | What to do |
|---|---|---|
| UTF-8 is the default text encoding | Programs on Windows that relied on cp1252 (or another locale encoding) read/write files differently. | Pass encoding= explicitly everywhere. Use encoding="locale" where you truly need the old behavior; PYTHONUTF8=0 as a temporary escape hatch. |
| Generational GC replaces incremental GC | Pause times and memory profile change in programs with big heaps; code tuned with gc.set_threshold for 3.14 may behave differently. | Re-measure peak memory and latency with your real workload before switching production. |
profile module deprecated | DeprecationWarning on import; scheduled for removal in 3.17. | Switch to profiling.tracing (or cProfile, which works on both versions). |
import lines in .pth files deprecated (PEP 829) | Packages that run startup code via .pth import lines emit warnings. | Ship a <name>.start file instead, listing pkg.mod:callable entry points; plain path lines in .pth files are still fine. |
New soft keyword lazy | Very low: soft keywords stay valid as names. | None, but avoid naming variables lazy in new code for readability. |
| Third-party wheels and conda packages | Compiled packages need 3.15 builds. | Solve a py315 Pixi environment early; the lockfile shows exactly which packages are available. |
Upgrade checklist
- Add a
py315feature and environment, and runpixi install -e py315 - Run the tests with warnings as errors:
pixi run -e py315 python -W error -m pytest - Grep for
open(calls withoutencoding=and fix them - Replace
import profilewithprofiling.tracingorcProfile - Benchmark memory and latency against 3.14
- Add 3.15 to the CI matrix before making it the default
Check yourself
Flashcards and further reading
Going further
- What's New In Python 3.15
- PEP 810, PEP 814, PEP 686, PEP 798, PEP 799
- Back to the baseline: What's New in Python 3.14