Modern Python features (3.11–3.14)
Exception groups, PEP 695 type params, PEP 701 f-strings, per-interpreter GIL, and free-threaded builds.
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
- Use features added in 3.10–3.14.
- Pin minimum Python version per project deliberately.
- 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)
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
def f(x: int | str) -> int | None: ...Concatenate, ParamSpec, TypeAlias
from typing import ParamSpec, TypeAlias
P = ParamSpec("P")
UserId: TypeAlias = intPrecise error locations
File "x.py", line 5, in main
result = data["users"][0].name
~~~~~~~~~~~~~~~~^^^^^
AttributeError: 'NoneType' object has no attribute 'name'zip(..., strict=True)
for a, b in zip([1, 2], ["a", "b"], strict=True):
...Parenthesised context managers
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)
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
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)
class Builder:
def add(self) -> Self: return selftomllib (read-only TOML)
import tomllib
cfg = tomllib.loads(Path("pyproject.toml").read_text())LiteralString for SQL injection prevention
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 stringUsed by mypy to detect injection vectors.
Variadic generics (PEP 646)
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
class M(TypedDict):
name: Required[str]
age: NotRequired[int]Datetime UTC alias
from datetime import datetime, UTC # = timezone.utc, shorter
datetime.now(UTC)Python 3.12 (2023)
PEP 695 — new type-parameter syntax
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
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
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 Basepathlib.Path.walk
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)
uv python install 3.13t
uv run --python 3.13t python -c "import sys; print(sys._is_gil_enabled())" # FalseSingle-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/quitwork without parens.
(The default REPL became pyrepl from PyPy.)
copy.replace for any object
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.
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:
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
| Old | New |
|---|---|
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 |
requests | httpx |
os.path | pathlib.Path |
time.time() for intervals | time.monotonic() / time.perf_counter() |
asyncio.gather | asyncio.TaskGroup (3.11+) |
asyncio.wait_for | async with asyncio.timeout(...) (3.11+) |
Self = TypeVar(...) | from typing import Self (3.11+) |
pickle for data | json / msgpack / msgspec |
% formatting and .format() | f-strings |
Custom cmp_to_key | key= with a tuple |
if x is True: | if x: (usually) |
Choosing a minimum Python version
| Version | When to target |
|---|---|
| 3.12 | Default for new libraries (broadly supported). |
| 3.11 | If you need wide cloud support; many platforms still default to it. |
| 3.13 | Cutting-edge libraries; pyrepl + better diagnostics. |
| 3.14 | Greenfield 2026 projects; ship with free-threaded support. |
Set in pyproject.toml:
[project]
requires-python = ">=3.12"And in [tool.ruff] / [tool.mypy]:
target-version = "py312" # ruff
python_version = "3.12" # mypyDon'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). Usesetuptools/hatchling.impmodule — gone. Useimportlib.asyncio.coroutinedecorator — gone. Useasync def.asyncio.get_event_loop()outside a running loop — deprecated. Useasyncio.runorget_running_loop.dict.has_key— long gone. Usein.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)
- Pick a recent script you wrote. Identify uses of
List,Dict,Union,Optional; modernise. - Replace any
try/finallycleanup withwithblocks where possible. - Replace
asyncio.gatherwithTaskGroupin an async script. - Use PEP 695 syntax for one generic class in your project.
- Replace one
time.time()-based timing withtime.perf_counter(). - Update
target-version/python_versioninpyproject.toml; runruff check . --fix(theUPrule modernises code automatically). - Bonus: try
uv python install 3.13tand verifysys._is_gil_enabled()is False.
Common pitfalls
- Using 3.12+ syntax in code targeting 3.11.
- Forgetting
from __future__ import annotationsin 3.10 codebases when forward references appear (not needed in 3.14+). - Relying on
asyncio.get_event_loop()outside a running loop (deprecated). - Using removed stdlib modules (
distutils, etc.) without realising they're gone. - Mixing TaskGroup and
gatherhaphazardly.
Self-check
- Which version added
TaskGroup? - What's PEP 695?
- Why use
f"{x=}"? - Difference between
Optional[int]andint | None? - What is a "t-string"?
References
- "What's New in Python 3.10/3.11/3.12/3.13/3.14" — official docs.
- PEP index: https://peps.python.org/.
- Łukasz Langa, "Faster CPython" project: https://github.com/faster-cpython.
- Brett Cannon, "Python launches" blog.
- Carl Meyer / Łukasz Langa — annotation evaluation talks.
Sign in to save your progress and earn badges.