What Is New in Python 3.12
Python 3.12 continues the performance work from 3.11, adding a new type parameter syntax for generics, f-string parser rewrite that unlocks previously invalid syntax, Linux perf profiler integration, and the largest stdlib cleanup in Python's history via PEP 594 removals. Sub-interpreters gain their own GIL, moving toward genuine interpreter-level isolation.
| Category | Change | PEP / Reference |
|---|---|---|
| New Syntax | Type parameter syntax: def fn[T](x: T) -> T |
PEP 695 |
| New Syntax | type statement for type aliases: type Point = tuple[float, float] |
PEP 695 |
| New Syntax | F-strings rewritten -- nested f-strings, quotes, multiline expressions allowed | PEP 701 |
| Performance | ~5% average speedup over 3.11; per-interpreter GIL for sub-interpreters | PEP 684 |
| Tooling | Linux perf profiler support via python -X perf |
-- |
| Standard Library | pathlib.Path.walk(); os.path.isreserved(); itertools.batched() |
-- |
| Removed | distutils, asynchat, asyncore, smtpd, lib2to3, imp |
-- |
| Deprecated | pkgutil.find_loader(); typing.AnyStr; non-path objects in pathlib |
-- |
What Is the New Type Parameter Syntax in Python 3.12?
PEP 695 introduces a compact syntax for defining generics directly in def, class, and the new type statement. Before 3.12, defining a generic function required importing TypeVar and declaring it separately -- now it is inline.
# Before 3.12
from typing import TypeVar
T = TypeVar("T")
def first(items: list[T]) -> T:
return items[0]
# Python 3.12+
def first[T](items: list[T]) -> T:
return items[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()
# Type alias
type Vector = list[float]
type Matrix[T] = list[list[T]]
The type statement creates a TypeAliasType object, which type checkers understand as a true alias rather than a variable assignment. This resolves an old ambiguity where MyList = list[str] looked like a regular assignment.
What Changed About F-Strings in Python 3.12?
The f-string parser was completely rewritten (PEP 701). The old parser was a hack bolted onto the tokenizer and had arbitrary restrictions. The new parser allows:
- Reuse of any quote type inside an expression:
f"{'nested string'}" - Nested f-strings:
f"{f"{f"{x}"}"}" - Multiline expressions inside
{}with comments - Backslashes inside expression blocks (previously a
SyntaxError)
# All these now work:
names = ["Alice", "Bob"]
print(f"{", ".join(names)}") # nested same-quote string
print(f"{"\n".join(names)}") # backslash in expression
total = f"{
sum(range(100)) # multiline with comment
}")
print(f"nested: {f"inner {1 + 1}"}")
How Does Linux perf Profiler Support Work in 3.12?
Running python -X perf script.py generates profiling data readable by the Linux perf tool. CPython emits DWARF frame information so that perf report shows Python function names alongside C stack frames -- giving a unified view of where time is spent across both layers.
# Profile with Linux perf
python -X perf -m my_app
# Then analyze
perf report
This is aimed at production profiling scenarios where you need to see the interaction between Python code and C extensions or system calls -- situations where cProfile alone is insufficient.
What Was Removed in Python 3.12?
Python 3.12 completed the removal of modules deprecated in 3.11 and earlier:
distutils-- usesetuptools(PyPI) or thebuildpackage instead.asynchat,asyncore-- useasyncio.smtpd-- useaiosmtpdor a dedicated SMTP library.lib2to3and the2to3tool -- Python 2 migration tools are obsolete.impmodule -- useimportlib.pkgutil.find_loader()-- useimportlib.util.find_spec().
If your code imports any of these, it will raise ModuleNotFoundError on Python 3.12. Migration is straightforward in all cases.
What New Standard Library Functions Shipped in 3.12?
pathlib.Path.walk()-- likeos.walk()but returnsPathobjects; supportstop_downandon_error.itertools.batched(iterable, n)-- yields tuples of up tonitems; clean chunking without manual slicing.os.process_cpu_count()-- counts CPUs available to the process (respects cgroups), unlikeos.cpu_count()which counts all physical CPUs.sys.monitoring-- low-overhead event monitoring for debuggers and profilers to hook into without patching bytecode.dataclasses.KW_ONLY-- marker to make all subsequent fields keyword-only in a dataclass definition.
FAQ
Does the new def fn[T]() syntax work in Python 3.11 and earlier?
No -- it is a 3.12+ syntax and causes a SyntaxError on older versions. If you need 3.11 compatibility, keep using TypeVar and the old import-based style. The __future__ module does not back-port this syntax.
Does the f-string rewrite change any existing f-string behavior?
No -- all existing valid f-strings produce identical results. The change only unlocks previously invalid constructs. This is purely additive: old code runs unchanged, and new code can take advantage of the relaxed grammar.
Does per-interpreter GIL (PEP 684) mean CPython finally has true thread parallelism in 3.12?
Not for normal threads. PEP 684 gives each sub-interpreter its own GIL, but sub-interpreters in 3.12 still cannot share Python objects between them safely. The work toward no-GIL free-threading started in 3.13 as an opt-in experimental build.
What should I use instead of distutils now that it is removed?
For packaging: setuptools (still on PyPI, has the same API) or the modern build backend ecosystem (build, hatch, flit). For C extension compilation: most projects use setuptools's Extension class. The pyproject.toml build system is the right long-term direction.
Is itertools.batched() equivalent to a manual islice loop?
Yes, semantically -- but cleaner. batched("ABCDEFG", 3) yields ('A','B','C'), ('D','E','F'), ('G',). The last batch is shorter if the input does not divide evenly. In 3.13, a strict=True option was added to raise if the final batch is short.