The features added in each recent release — what interviewers mean by “modern Python”, and what you'll see in current codebases.
At a glance
| Version | Headline features |
|---|---|
| 3.8 (2019) | Walrus :=, positional-only params /, f"{x=}", functools.cached_property, math.prod / isqrt |
| 3.9 (2020) | Dict merge | / |=, str.removeprefix / removesuffix, list[int] generics without typing, zoneinfo |
| 3.10 (2021) | match statement, X | Y union types, zip(strict=True), parenthesised with, much better error messages |
| 3.11 (2022) | 10–60% faster CPython, ExceptionGroup / except*, add_note, tomllib, asyncio.TaskGroup, typing.Self, error locations with ^^^ |
| 3.12 (2023) | Type-parameter syntax def f[T], type alias statement, f-strings may reuse quotes and nest freely, itertools.batched, @override |
| 3.13 (2024) | New colourful REPL, experimental free-threaded build (no GIL) and JIT, copy.replace, removed old “dead battery” modules |
| 3.14 (2025) | Deferred evaluation of annotations, template strings t"…", free-threading officially supported, except A, B without brackets, concurrent.interpreters, compression.zstd |
3.8 – 3.9: walrus, /, = debugging, dict union
import re
# Walrus: assign inside an expression — avoids calling twice
if (m := re.search(r"\d+", "order 66 shipped")):
print(m.group()) # 66
# Positional-only (/) and keyword-only (*) parameters
def clamp(x, /, lo=0, *, hi=10):
return max(lo, min(x, hi))
print(clamp(15), clamp(-3, lo=-1)) # 10 -1
# Self-documenting f-strings
width = 42
print(f"{width=}, {width * 2 = }") # width=42, width * 2 = 84
# 3.9: dict merge and prefix removal
defaults, user = {"theme": "light", "size": 12}, {"size": 14}
print(defaults | user) # {'theme': 'light', 'size': 14}
print("test_login".removeprefix("test_")) # login3.10: match, X | Y, strict zip
def http_status(code: int | None) -> str: # union without typing.Union
match code:
case 200 | 201:
return "ok"
case 404:
return "not found"
case int(n) if n >= 500:
return "server error"
case None:
return "no response"
case _:
return "other"
print(http_status(201), http_status(503), http_status(None)) # ok server error no response
try:
list(zip([1, 2, 3], ["a", "b"], strict=True)) # catch length bugs early
except ValueError as e:
print(e) # zip() argument 2 is shorter than argument 13.11: speed, exception groups, notes
3.11 was the first release of the Faster CPython project — an adaptive, specialising interpreter that made typical code 10–60% faster with no changes. It also made concurrent failures first-class.
def validate(user):
errors = []
if not user.get("name"):
errors.append(ValueError("name is required"))
if user.get("age", 0) < 0:
errors.append(ValueError("age must be >= 0"))
if errors:
raise ExceptionGroup("invalid user", errors)
try:
validate({"age": -1})
except* ValueError as eg: # handles every ValueError in the group
print(len(eg.exceptions), eg.exceptions[0]) # 2 name is required
try:
int("x")
except ValueError as e:
e.add_note("while parsing the 'qty' column") # extra context in the traceback
print(e.__notes__) # ["while parsing the 'qty' column"]import tomllib # read TOML config (stdlib since 3.11)
cfg = tomllib.loads("""
[tool.app]
debug = true
workers = 4
""")
print(cfg["tool"]["app"]) # {'debug': True, 'workers': 4}3.12: generics syntax & f-string freedom
from itertools import batched
def first[T](items: list[T]) -> T: # no TypeVar boilerplate
return items[0]
class Stack[T]:
def __init__(self) -> None:
self._items: list[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
return self._items.pop()
type Pair[T] = tuple[T, T] # lazily evaluated type alias
s = Stack[int]()
s.push(first([7, 8]))
print(s.pop(), Pair.__value__) # 7 tuple[T, T]
names = ["asha", "ravi"]
print(f"{", ".join(names)}") # asha, ravi ← same quotes inside, 3.12+
print(list(batched("abcdefg", 3))) # [('a', 'b', 'c'), ('d', 'e', 'f'), ('g',)]3.13: new REPL, free-threading, JIT
import copy
from dataclasses import dataclass
@dataclass(frozen=True)
class Point:
x: int
y: int
p = Point(1, 2)
print(copy.replace(p, y=5)) # Point(x=1, y=5)3.14: t-strings, lazy annotations, except A, B
Template strings look like f-strings but produce a Template object instead of a str, so a library can see the static text and the interpolated values separately — the safe way to build SQL, HTML or shell commands.
from string.templatelib import Template, Interpolation
def safe_html(tpl: Template) -> str:
out = []
for part in tpl: # static strings and Interpolations, in order
if isinstance(part, Interpolation):
out.append(str(part.value).replace("<", "<").replace(">", ">"))
else:
out.append(part)
return "".join(out)
user = "<script>"
tpl = t"<p>Hello {user}</p>"
print(type(tpl).__name__, tpl.values) # Template ('<script>',)
print(safe_html(tpl)) # <p>Hello <script></p># Annotations are evaluated lazily (only when asked for), so forward references just work:
def make_node() -> Node: # Node isn't defined yet — fine in 3.14
return Node()
class Node:
pass
print(type(make_node()).__name__) # Node
# Several exception types without brackets (when there's no "as"):
try:
int(None)
except ValueError, TypeError:
print("bad input") # bad input| Also in 3.14 | What it gives you |
|---|---|
| Free-threaded build supported | python3.14t is no longer experimental (still optional; the default build keeps the GIL) |
concurrent.interpreters | Several isolated interpreters in one process — parallelism without separate processes |
compression.zstd | Zstandard compression in the standard library |
finally warnings | return / break / continue inside finally now warns — it silently swallows exceptions |
| Better errors | Suggestions for mistyped keywords (whille → while?) |
Name three features added to Python in the last few versions.
What is the difference between an f-string and a t-string?