Overview
Python 3.12, released on October 2, 2023, is a major release that lays the groundwork for a future without a global GIL. PEP 684 introduces a per-interpreter GIL, allowing multiple sub-interpreters to run in true parallelism. F-strings become more flexible with nesting and comments, the new type syntax simplifies generic definitions, and error messages are even more precise.
Major Features
Per-interpreter GIL (PEP 684)
The GIL (Global Interpreter Lock) has historically prevented simultaneous execution of Python threads on multiple cores. PEP 684 gives each sub-interpreter its own GIL, meaning distinct sub-interpreters can execute Python code in true parallelism. This is a fundamental step toward the full GIL removal planned for Python 3.13.
For now, the C API is required to create sub-interpreters with separate GILs. The standard library interpreters module will arrive in a later release.
# Concept: sub-interpreters with separate GILs
# (underlying C API, conceptual illustration)
# Each sub-interpreter has:
# - Its own GIL
# - Its own imported modules
# - Its own global state
# In practice, for parallel computation on multiple cores,
# we still use multiprocessing or concurrent.futures
from concurrent.futures import ProcessPoolExecutor
import math
def estimate_pi(num_samples):
"""Estimate pi using Monte Carlo method."""
import random
inside_circle = 0
for _ in range(num_samples):
x = random.random()
y = random.random()
if x * x + y * y <= 1.0:
inside_circle += 1
return 4.0 * inside_circle / num_samples
# Parallel computation on 4 processes
with ProcessPoolExecutor(max_workers=4) as executor:
samples_per_worker = 2_500_000
futures = [
executor.submit(estimate_pi, samples_per_worker)
for _ in range(4)
]
estimates = [f.result() for f in futures]
estimated_pi = sum(estimates) / len(estimates)
print(f"Estimated pi: {estimated_pi:.6f}")
print(f"Actual pi: {math.pi:.6f}")
# Estimated pi: 3.141428
# Actual pi: 3.141593
F-string improvements (PEP 701)
Python 3.12 f-strings can now contain nested f-strings, span multiple lines, and include comments. The old parser imposed many restrictions that are lifted thanks to the new PEG parser introduced in Python 3.9.
# Nested f-strings (impossible before 3.12)
columns = ['name', 'age', 'city']
width = 15
header = f"{'|'.join(f'{col:^{width}}' for col in columns)}"
print(header)
# name | age | city
# Multi-line f-strings with comments
user = {'first': 'Alice', 'last': 'Smith', 'points': 1250}
message = f"Welcome {
user['first'] # user's first name
} {
user['last'] # family name
}, you have {
user['points'] # loyalty points
} points."
print(message)
# Welcome Alice Smith, you have 1250 points.
# Reusing identical quote characters
data = {'key': 'value'}
print(f"Result: {data['key']}")
# Result: value
Type parameter syntax (PEP 695)
PEP 695 introduces a new syntax for defining type aliases and generic classes. The type keyword replaces TypeAlias, and type parameters are expressed in square brackets directly after the class or function name. The code is more concise and readable than with manual annotations.
# Old style (Python < 3.12)
# from typing import TypeVar, TypeAlias, Generic
# T = TypeVar('T')
# Coordinates: TypeAlias = tuple[float, float]
# New style (Python 3.12+)
type Coordinates = tuple[float, float]
type Matrix[T] = list[list[T]]
# Generic class with the new syntax
class Stack[T]:
"""Typed generic stack."""
def __init__(self) -> None:
self._items: list[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
if not self._items:
raise IndexError('Stack is empty')
return self._items.pop()
def is_empty(self) -> bool:
return len(self._items) == 0
def __len__(self) -> int:
return len(self._items)
# Usage
int_stack = Stack[int]()
int_stack.push(10)
int_stack.push(20)
print(int_stack.pop()) # 20
str_stack = Stack[str]()
str_stack.push('hello')
print(len(str_stack)) # 1
# Generic function
def first[T](sequence: list[T]) -> T:
"""Return the first element of a sequence."""
return sequence[0]
print(first([3, 1, 4])) # 3
print(first(['a', 'b', 'c'])) # a
Improved error messages
Python 3.12 continues improving error messages with even more relevant suggestions. Import errors, attribute errors, and syntax errors now propose corrections when the name is close to a valid identifier. The message also indicates standard modules likely to provide the sought name.
# Suggestion for a typo on an attribute
# import math
# math.squart(16)
#
# AttributeError: module 'math' has no attribute 'squart'.
# Did you mean: 'sqrt'?
# Suggestion for a missing import
# from collections import ordereddict
#
# ImportError: cannot import name 'ordereddict' from 'collections'.
# Did you mean: 'OrderedDict'?
# Suggestion for a misspelled local variable
# counter = 0
# conter += 1
#
# NameError: name 'conter' is not defined.
# Did you mean: 'counter'?
# Suggestion for a misused keyword
# import = 42
#
# SyntaxError: invalid syntax
# Perhaps you meant 'import' as a keyword?
Minor Improvements
- The
pathlibmodule gainsPath.walk(), an alternative toos.walk(). - The
itertoolsmodule addsitertools.batched()for splitting an iterable into fixed-size batches. - Interpreter startup is faster thanks to module loading optimizations.
- Compiled files (
.pyc) use a new format with hash-based invalidation. - The
typingmodule gainsoverride(PEP 698) for marking methods that override a parent method.
Deprecations and Removals
Notable changes in this category:
- The
distutilsmodule is permanently removed (PEP 632); usesetuptoolsorsysconfiginstead. - The
asynchat,asyncore, andimpmodules are removed. - Long-deprecated
unittestfunctions are removed. - The
wtoolsmodule is removed.
