Reference · Python versions
What's New in Python 3.14
Python 3.14 is this course's baseline. This page walks through every headline change with a before/after example, a cell you can run right here (the browser runs real CPython 3.14 via Pyodide), and a link to the module that teaches it in depth.
- Recognize every major Python 3.14 language, stdlib, concurrency, typing, interpreter, and tooling change
- Rewrite older code to use the 3.14 idiom when it is clearer or safer
- Know which features you can try in the browser and which need a local Pixi environment
- Find the course module that teaches each feature in depth
At a glance
Python ships a new feature release every October (PEP 602). Each release gets about two years of bug fixes and then security fixes until five years after release. 3.14 is a big one: a new string-literal kind, a new model for annotations, an officially supported no-GIL build, multiple interpreters in the standard library, and a much friendlier REPL.
| Area | Change | PEP | Try in browser? | Taught in |
|---|---|---|---|---|
| Language | Template strings t"..." | 750 | Yes | M03 |
| Deferred evaluation of annotations | 649 / 749 | Yes | M16 | |
except A, B: without parentheses | 758 | Yes | M07 | |
Warnings for return/break/continue in finally | 765 | Yes | M07 | |
| Stdlib | compression.zstd and Zstandard in tarfile/zipfile/shutil | 784 | Maybe (depends on the Pyodide build) | M09 |
uuid.uuid7() | - | Yes | M40 | |
Path.copy() / Path.move() | - | Yes | M09 | |
Max-heap functions in heapq | - | Yes | M25 | |
map(..., strict=True) | - | Yes | M14 | |
functools.Placeholder | - | Yes | M18 | |
| Concurrency | Free-threaded build officially supported | 779 | No (local) | M37 |
concurrent.interpreters, InterpreterPoolExecutor | 734 | No (local) | M37 | |
python -m asyncio ps / pstree | - | No (local) | M38 | |
| Typing | annotationlib, get_annotations, Format | 649 / 749 | Yes | M16 |
| Interpreter | Tail-call interpreter | - | n/a | M39 |
| Experimental JIT in official builds | 744 | No (local) | M39 | |
| Incremental garbage collection | - | n/a | M39 | |
| Tooling | Syntax-highlighted, colored REPL | - | Local | M00 |
Color in unittest and argparse output | - | Local | M17, M40 | |
Safe external debugger attach: python -m pdb -p PID | 768 | No (local) | M43 |
Language changes
Template strings: t"..." (PEP 750)
An f-string is evaluated immediately into a plain str: by the time your code sees it, the
interpolated values are already glued into the text, so nothing can escape or validate them. A template
string uses the same syntax with a t prefix, but evaluates to a
string.templatelib.Template object that keeps the literal parts and the interpolated values
separate. A library function then decides how to combine them: escape HTML, parameterize SQL, build
structured log records, and so on.
Before (unsafe):
user = "<script>alert(1)</script>"
html = f"<p>Hello {user}</p>"
# the script tag is now inside htmlAfter (3.14):
user = "<script>alert(1)</script>"
html = render_html(t"<p>Hello {user}</p>")
# render_html escapes each interpolationEach Interpolation records the value, the source expression text, the
conversion ("r", "s", "a", or None), and the
format_spec. Templates are not strings: str(tpl) gives a debugging repr, not the
rendered text, which is deliberate so you cannot accidentally skip the processing step.
Deferred evaluation of annotations (PEP 649 / 749)
Before 3.14, annotations were evaluated eagerly when the def or class statement ran.
Referring to a class that was defined later in the file raised NameError, so people wrote string
annotations ("Node") or put from __future__ import annotations at the top of every file.
In 3.14 annotations are stored as a small function and evaluated only when someone asks for them, for example via
__annotations__ or annotationlib.get_annotations().
Before:
from __future__ import annotations # or "Node" strings
class Node:
def add(self, child: "Node") -> "Node": ...After (3.14):
class Node:
def add(self, child: Node) -> Node: ...
# no quotes, no __future__ importThe full story (formats, annotationlib) is in Typing below.
Bracketless except (PEP 758)
You may now list several exception types after except or except* without parentheses,
as long as you do not use as. With as name, the parentheses are still required,
because except A, B as e would be ambiguous to read.
Before:
try:
value = int(text)
except (ValueError, TypeError):
value = 0After (3.14):
try:
value = int(text)
except ValueError, TypeError:
value = 0Warnings for control flow in finally (PEP 765)
A return, break, or continue that leaves a finally block
silently discards any exception in flight, which hides real bugs. 3.14 emits a SyntaxWarning at
compile time for these. The code still runs, but linters and -W error builds will now flag it.
Before (bug hides):
def load():
try:
return parse() # raises
finally:
return None # swallows the errorAfter (clear):
def load():
try:
return parse()
finally:
cleanup() # no return here
Standard library
Zstandard compression: compression.zstd (PEP 784)
Zstandard compresses about as well as gzip at a fraction of the CPU
time and decompresses very fast, which is why Parquet files, Linux packages, and many data pipelines use it.
3.14 adds the compression package with compression.zstd, and re-exports the old modules as
compression.gzip, compression.bz2, compression.lzma, and
compression.zlib. tarfile, zipfile, and shutil learned to read
and write Zstandard archives too (for example tarfile.open(path, "w:zst"),
zipfile.ZIP_ZSTANDARD, and the "zstdtar" archive format).
Before: a third-party package
pixi add zstandardAfter (3.14): built in
from compression import zstd
blob = zstd.compress(data)Time-ordered IDs: uuid.uuid7()
uuid4() is fully random, so new rows land at random places in a database index, which fragments it.
A UUIDv7 starts with a 48-bit millisecond Unix timestamp, so IDs generated later sort later: great
primary keys that are still globally unique. 3.14 also adds uuid6() and uuid8(), plus
the NIL and MAX constants.
Copy and move with pathlib
Path.copy(target) copies a file or a whole directory tree, and Path.move(target) moves
one. Both return the new Path. There are also copy_into(dir) and
move_into(dir). You no longer need to mix pathlib with shutil for everyday
file management.
Before:
import shutil
shutil.copy2(src, dst)
shutil.move(str(dst), str(final))After (3.14):
dst = src.copy(dst)
final = dst.move(final)Max-heaps in heapq
heapq has always been a min-heap. To get a max-heap you pushed negated numbers
(-x), which breaks for non-numeric items. 3.14 adds heapify_max,
heappush_max, heappop_max, heappushpop_max, and
heapreplace_max.
Before:
heap = [-x for x in scores]
heapq.heapify(heap)
best = -heapq.heappop(heap)After (3.14):
heap = list(scores)
heapq.heapify_max(heap)
best = heapq.heappop_max(heap)map(strict=True)
Since 3.10, zip(strict=True) raises if its iterables have different lengths. 3.14 gives
map the same safety switch for multi-iterable calls, so a silently truncated result becomes a loud
ValueError.
functools.Placeholder
functools.partial could only pre-fill arguments from the left (or by keyword). Many built-ins take
positional-only arguments, so you could not freeze, say, the second argument of str.replace.
Placeholder reserves a positional slot that is filled when the partial is called.
Before:
underscore = lambda s: s.replace(" ", "_")After (3.14):
from functools import partial, Placeholder as _
underscore = partial(str.replace, _, " ", "_")Concurrency
Free-threaded Python is officially supported (PEP 779)
The GIL (global interpreter lock) lets only one thread run Python bytecode at a time, so CPU-bound
threads never ran in parallel. 3.13 introduced an experimental build without the GIL; in 3.14 that
free-threaded build (python3.14t) moves to "officially supported" (phase II). It is still a
separate build, not the default. Single-threaded code runs a little slower on it, and C extensions must declare
support, but CPU-bound threads finally scale across cores.
Multiple interpreters: concurrent.interpreters (PEP 734)
A subinterpreter is a separate Python interpreter inside the same process, each with its own modules
and (since 3.12) its own GIL. They give process-like isolation with thread-like cost. 3.14 exposes them in the
stdlib as concurrent.interpreters and adds concurrent.futures.InterpreterPoolExecutor,
which has the same API as the thread and process pools.
Before: processes for CPU parallelism
from concurrent.futures import ProcessPoolExecutor
with ProcessPoolExecutor() as pool:
results = list(pool.map(work, items))After (3.14): interpreters
from concurrent.futures import InterpreterPoolExecutor
with InterpreterPoolExecutor() as pool:
results = list(pool.map(work, items))Inspecting running asyncio programs
When an async service hangs, you want to know which tasks exist and what each is awaiting. 3.14 can show you
from outside the process: python -m asyncio ps PID prints a table of tasks, and
python -m asyncio pstree PID shows the await tree. It uses the same safe-attach machinery as remote
pdb (below). Inside a program, asyncio.capture_call_graph() and
asyncio.print_call_graph() give the same information.
# shell 1
pixi run python crawler.py # prints its PID, then runs for a while
# shell 2
pixi run python -m asyncio ps 12345
pixi run python -m asyncio pstree 12345
Typing and annotations
With deferred evaluation, an annotation may refer to names that do not exist (yet). Asking for its
values would then raise NameError. The new annotationlib module lets tools ask
for annotations in different formats:
annotationlib.Format | Returns | Use it for |
|---|---|---|
VALUE | Real objects (the default; may raise NameError) | Runtime validation (dataclasses, pydantic) |
FORWARDREF | Real objects where possible, ForwardRef placeholders otherwise | Libraries that must not crash on missing names |
STRING | The annotation source text | Documentation tools, help output |
Performance and the interpreter
Tail-call interpreter
A new way of compiling CPython's bytecode loop in which each opcode handler is a small C function that tail-calls the next one. With recent Clang it gives a few percent speedup on typical benchmarks. It's an internal change: your code does not change.
Experimental JIT
Official Windows and macOS binaries now include the copy-and-patch JIT
(PEP 744), off by default. Enable it with
PYTHON_JIT=1. Speedups in 3.14 are modest and vary.
Incremental GC
The cyclic garbage collector collects the old generation a slice at a time, which cuts long pause times in programs with large heaps. (3.15 reverts to the generational collector; see the 3.15 preview.)
# is the JIT compiled in, and is it on?
PYTHON_JIT=1 pixi run python -c "import sys; print(sys._jit.is_available(), sys._jit.is_enabled())"
Tooling and the REPL
A colorful, smarter REPL
The interactive shell that 3.13 rewrote now syntax-highlights code as you type, and
tracebacks are in color. Import statements autocomplete module names (import co then
Tab). Colors follow the PYTHON_COLORS, NO_COLOR, and
FORCE_COLOR environment variables.
pixi run python # the 3.14 REPL with highlighting
NO_COLOR=1 pixi run python # plain output (for screen readers or logs)
Color in unittest, argparse, and more
unittest output colors passes and failures. argparse.ArgumentParser has a
color parameter (on by default) that colors help text, and a suggest_on_error option
that suggests the closest valid choice when you mistype one. json, calendar, and other
command-line tools gained color too.
Attach a debugger to a running process (PEP 768)
3.14 adds a safe hook that lets an external tool ask a running interpreter to execute code at a safe point. The
first user is pdb: python -m pdb -p PID attaches to a live process, without restarting it or having
added any debugging code. (For production use you can disable it with PYTHON_DISABLE_REMOTE_DEBUG or
-X disable-remote-debug.)
pixi run python long_job.py & # prints nothing, runs for minutes
pixi run python -m pdb -p $! # attach to it and inspect frames
Upgrading a project to 3.14
- Pin it:
pixi add "python=3.14.*", thenpixi installupdatespixi.lock. - Run the full suite with warnings as errors once:
pixi run python -W error -m pytest. NewSyntaxWarnings from PEP 765 and deprecations show up here. - Drop
from __future__ import annotationsand quoted forward references when you are ready (ruff'sUPrules help); code that reads__annotations__should switch toannotationlib.get_annotations. - Set mypy's
python_versionto your lowest supported version, not 3.14, so 3.14-only APIs are caught if you still support 3.12. - Test a CI matrix across the supported versions using Pixi features (see the Pixi cheat sheet).
Check yourself
Flashcards and further reading
Going further
- What's New In Python 3.14 (the official, complete list)
- PEP 750, PEP 649, PEP 779, PEP 734, PEP 768
- Free-threaded Python guide
- Next: Preview: Python 3.15