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.

⏱ ~60 min to read and try everything3.14 released October 2025Official notes: What's New In Python 3.14

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.

AreaChangePEPTry in browser?Taught in
LanguageTemplate strings t"..."750YesM03
Deferred evaluation of annotations649 / 749YesM16
except A, B: without parentheses758YesM07
Warnings for return/break/continue in finally765YesM07
Stdlibcompression.zstd and Zstandard in tarfile/zipfile/shutil784Maybe (depends on the Pyodide build)M09
uuid.uuid7()-YesM40
Path.copy() / Path.move()-YesM09
Max-heap functions in heapq-YesM25
map(..., strict=True)-YesM14
functools.Placeholder-YesM18
ConcurrencyFree-threaded build officially supported779No (local)M37
concurrent.interpreters, InterpreterPoolExecutor734No (local)M37
python -m asyncio ps / pstree-No (local)M38
Typingannotationlib, get_annotations, Format649 / 749YesM16
InterpreterTail-call interpreter-n/aM39
Experimental JIT in official builds744No (local)M39
Incremental garbage collection-n/aM39
ToolingSyntax-highlighted, colored REPL-LocalM00
Color in unittest and argparse output-LocalM17, M40
Safe external debugger attach: python -m pdb -p PID768No (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 html

After (3.14):

user = "<script>alert(1)</script>"
html = render_html(t"<p>Hello {user}</p>")
# render_html escapes each interpolation

Each 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__ import

The 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 = 0

After (3.14):

try:
    value = int(text)
except ValueError, TypeError:
    value = 0

Warnings 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 error

After (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 zstandard

After (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.FormatReturnsUse it for
VALUEReal objects (the default; may raise NameError)Runtime validation (dataclasses, pydantic)
FORWARDREFReal objects where possible, ForwardRef placeholders otherwiseLibraries that must not crash on missing names
STRINGThe annotation source textDocumentation 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

  1. Pin it: pixi add "python=3.14.*", then pixi install updates pixi.lock.
  2. Run the full suite with warnings as errors once: pixi run python -W error -m pytest. New SyntaxWarnings from PEP 765 and deprecations show up here.
  3. Drop from __future__ import annotations and quoted forward references when you are ready (ruff's UP rules help); code that reads __annotations__ should switch to annotationlib.get_annotations.
  4. Set mypy's python_version to your lowest supported version, not 3.14, so 3.14-only APIs are caught if you still support 3.12.
  5. Test a CI matrix across the supported versions using Pixi features (see the Pixi cheat sheet).

Check yourself

Flashcards and further reading

Going further