Modern Python features (3.11–3.14)

Exception groups, PEP 695 type params, PEP 701 f-strings, per-interpreter GIL, and free-threaded builds.

📦 Module 4 8 min read Not started

Why this matters

Python evolves every year. Between 3.10 and 3.14 the language picked up structural pattern matching, exception groups, TaskGroup, Self, tomllib, performance gains, PEP 695 type-parameter syntax, free-threaded build, and more. Knowing which features exist (and which version they need) keeps you from re-inventing things and unlocks cleaner code.

Learning objectives

  1. Use features added in 3.10–3.14.
  2. Pin minimum Python version per project deliberately.
  3. Avoid features your deployment target doesn't have.

This is a quick reference — cherry-pick what you need.


Python 3.10 (2021)

Structural Pattern Matching (PEP 634)

python
match msg:
    case {"type": "ping"}: ...
    case {"type": "echo", "text": str(t)}: ...
    case [first, *rest]: ...
    case Point(x=0, y=0): ...
    case _: ...

match patterns: literal, capture, class, sequence, mapping, OR (|), guard (if).

Union syntax X | Y

python
def f(x: int | str) -> int | None: ...

Concatenate, ParamSpec, TypeAlias

python
from typing import ParamSpec, TypeAlias
P = ParamSpec("P")
UserId: TypeAlias = int

Precise error locations

File "x.py", line 5, in main
    result = data["users"][0].name
             ~~~~~~~~~~~~~~~~^^^^^
AttributeError: 'NoneType' object has no attribute 'name'

zip(..., strict=True)

python
for a, b in zip([1, 2], ["a", "b"], strict=True):
    ...

Parenthesised context managers

python
with (
    open("a") as a,
    open("b") as b,
):
    ...

Python 3.11 (2022) — the "10-60% faster" release

Speedups

~25% average speedup vs 3.10 from the "Faster CPython" project (specialising adaptive interpreter, frame optimisations).

Exception Groups + except* (PEP 654)

python
try:
    ...
except* ValueError as eg:
    for e in eg.exceptions: log(e)
except* TimeoutError as eg:
    ...

asyncio.TaskGroup (also 3.11) raises an ExceptionGroup when multiple tasks fail.

asyncio.TaskGroup and asyncio.timeout

python
async with asyncio.TaskGroup() as tg:
    tg.create_task(work())
    tg.create_task(other())

async with asyncio.timeout(5):
    await slow_thing()

Self type (PEP 673)

python
class Builder:
    def add(self) -> Self: return self

tomllib (read-only TOML)

python
import tomllib
cfg = tomllib.loads(Path("pyproject.toml").read_text())

LiteralString for SQL injection prevention

python
from typing import LiteralString
def execute(query: LiteralString, params: tuple) -> None: ...
execute("SELECT * FROM users", ())                  # ok
execute(f"SELECT * FROM users WHERE id={uid}", ())   # mypy error: not a literal string

Used by mypy to detect injection vectors.

Variadic generics (PEP 646)

python
from typing import TypeVarTuple, Unpack
Ts = TypeVarTuple("Ts")
class Tensor(Generic[Unpack[Ts]]): ...

(Mainly used by tensor libraries.)

Fine-grained tracebacks

Carets ^^^^ point at exact expression. Lifesaver.

Required / NotRequired for TypedDict

python
class M(TypedDict):
    name: Required[str]
    age: NotRequired[int]

Datetime UTC alias

python
from datetime import datetime, UTC          # = timezone.utc, shorter
datetime.now(UTC)

Python 3.12 (2023)

PEP 695 — new type-parameter syntax

python
type Vector = list[float]

def first[T](xs: list[T]) -> T: return xs[0]

class Stack[T]:
    def push(self, item: T) -> None: ...

No more TypeVar, Generic, TypeAlias boilerplate for the common case.

itertools.batched

python
from itertools import batched
for chunk in batched(ids, 100): ...

F-string improvements (PEP 701)

  • Nested quotes: f"{'inner'}" works.
  • Backslashes/newlines allowed inside expressions.
  • Comments inside { ... } (3.12+).

Performance improvements (continued)

~5% faster vs 3.11. New per-interpreter GIL (PEP 684) — enables true sub-interpreter parallelism (still experimental for end users).

@override decorator

python
from typing import override

class Base:
    def method(self) -> None: ...

class Child(Base):
    @override
    def method(self) -> None: ...    # type-checker errors if `method` removed from Base

pathlib.Path.walk

python
for root, dirs, files in Path("/").walk():
    ...

Replaces os.walk with a Path-returning version.

sys.monitoring

New low-overhead instrumentation API (used by profilers like py-spy and debuggers).


Python 3.13 (2024) — free-threaded preview

Experimental free-threaded build (PEP 703)

powershell
uv python install 3.13t
uv run --python 3.13t python -c "import sys; print(sys._is_gil_enabled())"   # False

Single-threaded ~5-10% slower; multi-threaded CPU work scales. Some C extensions need updates; not all third-party libs work yet.

Experimental JIT (PEP 744)

Copy-and-patch JIT compiler. Builds with --enable-experimental-jit. Modest speedups on some workloads; will improve over 3.14/3.15.

Improved error messages

Even better hints. NameError: "did you mean ...?" Suggestions for typos.

REPL improvements

  • Multi-line editing with <Esc>+Enter.
  • Syntax highlighting.
  • exit/quit work without parens.

(The default REPL became pyrepl from PyPy.)

copy.replace for any object

python
import copy
new = copy.replace(obj, field=new_value)

Works for dataclasses, NamedTuples, and any object implementing __replace__.

Removed: distutils, dead-code modules

The "PEP 594 dead batteries" finally got removed. Migrate to setuptools / hatchling for builds.

Mobile platform support (PEP 730/738)

iOS and Android Tier-3 supported.


Python 3.14 (2025)

Deferred evaluation of annotations (PEP 649 + 749)

Annotations are evaluated lazily — fixes years of forward-reference pain. You can drop from __future__ import annotations.

Free-threaded build → Tier 1 (PEP 779)

Free-threaded becomes officially supported (no longer "experimental"). Wheels are starting to ship with both standard and free-threaded variants.

concurrent.interpreters (PEP 734)

Standard-library wrapper around subinterpreters for parallel Python without subprocess overhead.

python
import concurrent.interpreters as ci
interp = ci.create()
interp.exec("x = 1 + 2")

Tail-call interpreter

Faster bytecode dispatch — another step in the Faster CPython work.

t-strings — template strings (PEP 750)

A new string prefix for safe interpolation:

python
from string.templatelib import Template

query = t"SELECT * FROM users WHERE id = {user_id}"
# Type is `Template`, not `str` — libraries can render it safely (no SQL injection by default).

Big for ORMs, HTML rendering, shell command construction. Adoption ongoing through 2026.

Improved type narrowing for isinstance chains, exhaustive match.

pyrepl is now the default interactive REPL on Windows too.


Migration cheat sheet

OldNew
List[int]list[int]
Optional[int]int | None
Union[int, str]int | str
from typing import TypeVar; T = TypeVar(...)def f[T](...) (3.12+)
try/finally: cleanup()with
requestshttpx
os.pathpathlib.Path
time.time() for intervalstime.monotonic() / time.perf_counter()
asyncio.gatherasyncio.TaskGroup (3.11+)
asyncio.wait_forasync with asyncio.timeout(...) (3.11+)
Self = TypeVar(...)from typing import Self (3.11+)
pickle for datajson / msgpack / msgspec
% formatting and .format()f-strings
Custom cmp_to_keykey= with a tuple
if x is True:if x: (usually)

Choosing a minimum Python version

VersionWhen to target
3.12Default for new libraries (broadly supported).
3.11If you need wide cloud support; many platforms still default to it.
3.13Cutting-edge libraries; pyrepl + better diagnostics.
3.14Greenfield 2026 projects; ship with free-threaded support.

Set in pyproject.toml:

toml
[project]
requires-python = ">=3.12"

And in [tool.ruff] / [tool.mypy]:

toml
target-version = "py312"     # ruff
python_version = "3.12"      # mypy

Don't over-target. If you use match, your minimum is 3.10. If TaskGroup, 3.11. If new generics syntax, 3.12.


Removed / deprecated

  • distutils — gone (3.12). Use setuptools / hatchling.
  • imp module — gone. Use importlib.
  • asyncio.coroutine decorator — gone. Use async def.
  • asyncio.get_event_loop() outside a running loop — deprecated. Use asyncio.run or get_running_loop.
  • dict.has_key — long gone. Use in.
  • cgi, cgitb, crypt, imghdr, mailcap, msilib, nis, ossaudiodev, pipes, smtpd, sndhdr, spwd, sunau, telnetlib, uu, xdrlib, nntplib, chunk — PEP 594 dead batteries removed in 3.13.

If you depend on any of these, find a third-party replacement.


Hands-on lab (1 hour)

  1. Pick a recent script you wrote. Identify uses of List, Dict, Union, Optional; modernise.
  2. Replace any try/finally cleanup with with blocks where possible.
  3. Replace asyncio.gather with TaskGroup in an async script.
  4. Use PEP 695 syntax for one generic class in your project.
  5. Replace one time.time()-based timing with time.perf_counter().
  6. Update target-version / python_version in pyproject.toml; run ruff check . --fix (the UP rule modernises code automatically).
  7. Bonus: try uv python install 3.13t and verify sys._is_gil_enabled() is False.

Common pitfalls

  1. Using 3.12+ syntax in code targeting 3.11.
  2. Forgetting from __future__ import annotations in 3.10 codebases when forward references appear (not needed in 3.14+).
  3. Relying on asyncio.get_event_loop() outside a running loop (deprecated).
  4. Using removed stdlib modules (distutils, etc.) without realising they're gone.
  5. Mixing TaskGroup and gather haphazardly.

Self-check

  1. Which version added TaskGroup?
  2. What's PEP 695?
  3. Why use f"{x=}"?
  4. Difference between Optional[int] and int | None?
  5. What is a "t-string"?

References

Sign in to save your progress and earn badges.