What's New in Python 3.12¶
3.12
Python 3.12 was released in October 2023. The headliners are much more flexible f-strings, the new type statement for type aliases, @typing.override, and the sys.monitoring API for building profilers.
Summary¶
| Category | Change |
|---|---|
| Language | F-string improvements (PEP 701) |
| Language | type statement for type aliases (PEP 695) |
| Typing | @typing.override decorator (PEP 698) |
| CPython | sys.monitoring API (PEP 669) |
| Standard Library | pathlib.Path.walk() |
| Standard Library | itertools.batched() |
| Performance | ~5% faster than 3.11 on average |
PEP 701 — Better F-Strings¶
Before 3.12, f-strings had a bunch of annoying restrictions: you couldn't reuse the same quote type inside the expression, you couldn't have backslashes inside {}, and multi-line f-strings were painful.
3.12 fixes all of that:
# Previously invalid — quote type collision
name = "world"
# msg = f"Hello {"world"}" # SyntaxError before 3.12
# Now valid in 3.12!
msg = f"Hello {"world"}"
print(msg) # Hello world
# Backslashes inside f-string expressions — now valid
items = ["a", "b", "c"]
print(f"Joined: {'\n'.join(items)}")
# Nested f-strings — now properly supported
x = 42
print(f"{'positive' if x > 0 else 'negative'}")
# Multi-line f-string expressions
result = f"""
Name: {"Alice"}
Score: {
100 +
200 +
300
}
"""
PEP 695 — type Statement for Type Aliases¶
Before 3.12, creating a type alias was a bit awkward:
Python 3.12 introduces the type keyword:
# New in 3.12 — clean and unambiguous
type Vector = list[float]
type Matrix[T] = list[list[T]] # Generic type alias
# Use it
def scale(v: Vector, factor: float) -> Vector:
return [x * factor for x in v]
It also works for generic functions and classes:
# Generic function — no need to import TypeVar
def first[T](lst: list[T]) -> T:
return lst[0]
# Generic class
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()
@typing.override¶
Marks a method as intentionally overriding a parent class method. Type checkers will error if the parent class doesn't have a matching method — catching silent breakage when you rename a base class method.
from typing import override
class Base:
def process(self) -> None:
print("base")
class Child(Base):
@override
def process(self) -> None: # OK — Base.process exists
print("child")
class Broken(Base):
@override
def prcess(self) -> None: # Type checker error! Typo — no Base.prcess
print("oops")
sys.monitoring — PEP 669¶
A low-overhead monitoring API for profilers and debuggers. Instead of sys.settrace() (which has ~10x overhead), sys.monitoring lets you subscribe to specific events at minimal cost.
import sys
# Enable function call monitoring
sys.monitoring.set_events(
sys.monitoring.DEBUGGER_ID,
sys.monitoring.events.CALL
)
def on_call(code, instruction_offset, callable, arg0):
print(f"Called: {callable.__name__}")
sys.monitoring.register_callback(
sys.monitoring.DEBUGGER_ID,
sys.monitoring.events.CALL,
on_call
)
New Standard Library Features¶
pathlib.Path.walk()¶
Path objects finally have a walk() method, equivalent to os.walk():
from pathlib import Path
for dirpath, dirnames, filenames in Path(".").walk():
for f in filenames:
print(dirpath / f)
itertools.batched()¶
Split an iterable into fixed-size chunks — a common need that previously required a recipe: