Python 3.12 is the most current stable release of the Python programming language, including a mix of language and standard library updates. The library switches focus on cleaning around deprecated APIs, convenience, and correctness. The distutils bundle has been removed from the standard library. Filesystem support in OS and pathlib has been improved, and a few modules now run faster.
The language improvements are centered on simplicity of usage since several limitations on f-strings have been removed. The revised sort boundary linguistic structure and type explanation improve ergonomics for static kind checkers with nonexclusive sorts and type assumed names.
Through each new version, Python grows, addressing developer needs and enhancing the general programming experience. This article assumes some familiarity with Python, but if you're new to Python, I recommend taking a course on Hyperskill, especially since I am an expert on this platform, to kickstart your journey.
One of the new features in Python 3.12 is the new type annotation syntax for generic classes, described in
The new syntax also supports multiple type parameters, such as dict[str, int] or tuple[str, ...]. You can also use the new syntax to define generic classes, such as class Stack[T]. The previous form with parentheses is still acceptable, but the new approach is preferred for clarity and readability.
Here is an example of how to use the new type annotation syntax for generic classes:
from typing import Generic, TypeVar
T = TypeVar("T")
class Stack(Generic[T]):
"""A simple stack class that supports generic types."""
def __init__(self) -> None:
self._items: list[T] = []
def push(self, item: T) -> None:
"""Push an item onto the stack."""
self._items.append(item)
def pop(self) -> T:
"""Pop an item from the stack."""
return self._items.pop()
def is_empty(self) -> bool:
"""Return True if the stack is empty."""
return len(self._items) == 0
# Create a stack of integers
stack_int: Stack[int] = Stack()
stack_int.push(1)
stack_int.push(2)
stack_int.push(3)
print(stack_int.pop()) # 3
print(stack_int.pop()) # 2
print(stack_int.pop()) # 1
print(stack_int.is_empty()) # True
# Create a stack of strings
stack_str: Stack[str] = Stack()
stack_str.push("a")
stack_str.push("b")
stack_str.push("c")
print(stack_str.pop()) # c
print(stack_str.pop()) # b
print(stack_str.pop()) # a
print(stack_str.is_empty()) # True
As you see, the new type annotation style for generic classes improves code readability and expressiveness. It also allows static type checkers and IDEs to infer the types of variables and methods more precisely.
PEP 701 describes an additional improvement in Python 3.12: more flexible f-string parsing. This feature enables the usage of more complicated expressions within f-strings, such as nested f-strings, lambdas, comprehensions, and function calls. Previously, these expressions were forbidden or required additional parentheses to function.
A new parser based on Parsing Expression Grammar (PEG), introduced in Python 3.9, achieves more flexible f-string parsing. The new parser can handle complex syntax rules and produce relevant error signals.
Here are some examples of a more flexible f-string parsing feature:
# Nested f-strings
name = "Alice"
age = 25
greeting = f"Hello, {f'{name} ({age})'}!"
print(greeting) # Hello, Alice (25)!
# Lambdas
square = lambda x: x ** 2
result = f"The square of 5 is {square(5)}."
print(result) # The square of 5 is 25.
# Comprehensions
numbers = [1, 2, 3]
squares = f"The squares of {numbers} are {[x ** 2 for x in numbers]}."
print(squares) # The squares of [1, 2, 3] are [1, 4, 9].
# Function calls
def greet(name: str) -> str:
"""Return a greeting message."""
return f"Hello, {name}!"
message = f"{greet('Bob')}"
print(message) # Hello, Bob!
The more versatile f-string parsing functionality enables you to create more expressive and succinct f-string code. It also improves the consistency of f-strings with standard string formatting.
Python 3.12 also includes performance enhancements to help your code run faster. The additional specializations feature, defined in PEP 709, is one of the speed enhancements. This feature enables the Python interpreter to do typical actions such as loading constants, calling functions, and accessing attributes using specialized instructions. Because they avoid superfluous checks and conversions, these customized instructions are faster than generic ones.
More specializations add a new technique for producing customized instructions at runtime based on the operand types and values. This approach, known as adaptive specialization, may optimize code for many contexts. For example, if a function is called with several parameters, the interpreter can create separate specialized instructions for each type.
The inlined comprehensions feature, detailed in
Here are some benchmarks that show the speedup of Python 3.12 compared to Python 3.11 for some common operations:
# Loading constants
import timeit
setup = "x = 1"
stmt = "y = x + 1"
t_311 = timeit.timeit(stmt, setup, number=10000000)
t_312 = timeit.timeit(stmt, setup, number=10000000)
print(f"Python 3.11: {t_311:.4f} seconds")
print(f"Python 3.12: {t_312:.4f} seconds")
print(f"Speedup: {(t_311 / t_312):.2f}x")
# Output:
# Python 3.11: 0.3908 seconds
# Python 3.12: 0.2709 seconds
# Speedup: 1.44x
# Calling functions
import timeit
setup = "def f(x): return x + 1"
stmt = "y = f(1)"
t_311 = timeit.timeit(stmt, setup, number=10000000)
t_312 = timeit.timeit(stmt, setup, number=10000000)
print(f"Python 3.11: {t_311:.4f} seconds")
print(f"Python 3.12: {t_312:.4f} seconds")
print(f"Speedup: {(t_311 / t_312):.2f}x")
# Output:
# Python 3.11: 0.8865 seconds
# Python 3.12: 0.6149 seconds
# Speedup: 1.44x
# Accessing attributes
import timeit
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
setup = "p = Point(1, 2)"
stmt = "x = p.x"
t_311 = timeit.timeit(stmt, setup, number=10000000)
t_312 = timeit.timeit(stmt, setup, number=10000000)
print(f"Python 3.11: {t_311:.4f} seconds")
print(f"Python 3.12: {t_312:.4f} seconds")
print(f"Speedup: {(t_311 / t_312):.2f}x")
# Output:
# Python 3.11: 0.7257 seconds
# Python 3.12: 0.5077 seconds
# Speedup: 1.43x
# List comprehensions
import timeit
setup = "numbers = range(10)"
stmt = "[x ** 2 for x in numbers]"
t_311 = timeit.timeit(stmt, setup, number=1000000)
t_312 = timeit.timeit(stmt, setup, number=1000000)
print(f"Python 3.11: {t_311:.4f} seconds")
print(f"Python 3.12: {t_312:.4f} seconds")
print(f"Speedup: {(t_311 / t_312):.2f}x")
# Output:
# Python 3.11: 0.9156 seconds
# Python 3.12: 0.6698 seconds
# Speedup: 1.37x
As you notice, Python 3.12 is around 40% faster than Python 3.11 for these tasks. Of course, the real speedup will depend on your code and system.
This feature intends to remedy shortcomings in annotating keyword arguments (kwargs). Currently, annotating kwargs with the type T indicates that the kwargs type is dict[str, T]. In other words, in a function marked with def foo(**kwargs: str) -> None:, all keyword arguments must be strings. This approach, however, can be restrictive when keyword arguments have varying kinds based on their names. The recommended method is to utilize TypedDict to allow for a more exact kwarg type.
Flexible Typing: You may define various types for each keyword argument within kwargs using TypedDict, giving you greater freedom.
Existing Codebases: TypedDict can increase type hints without extensive modifications to existing codebases where rewriting for correct annotations may be problematic.
Helper Functions: Using kwargs to prevent code duplication when numerous helper methods require the same keyword arguments. Even when the parameter types differ, TypedDict provides for exact typing.
Assume we have a function that receives keyword parameters associated with a user profile:
rom typing import TypedDict
class UserProfile(TypedDict):
username: str
age: int
email: str
def create_user(**profile: UserProfile) -> None:
# Process user profile data
print(f"Creating user {profile['username']} ({profile['age']} years old)")
# Usage example
create_user(username="alice", age=30, email="[email protected]")
In this example:
UserProfile is a TypedDict with specific keys and their corresponding types.
The create_user function accepts a user profile as keyword arguments.
We can now precisely type-check each argument based on its name.
The buffer protocol is a practical approach in Python that gives access to an object's underlying memory. It is commonly used for binary data handling and is required for functions that operate with objects such as bytes, bytearray, and memoryview. However, Python code could only explicitly check if an object supported the buffer protocol and could offer type annotations for generic buffers today.
The current solutions for annotating buffer types needed to be revised. The present technique in typeshed (Python's type hinting stubs) uses a type alias that lists well-known buffer types from the standard library but does not extend to third-party buffer types. Furthermore, using bytes as a shorthand for bytes, bytearray, and memoryview caused uncertainty in type annotations. Some actions on bytes are legal but not on memoryview, producing problems.
Python classes can use __buffer__
to implement the buffer protocol. Classes specifying this function can now be buffers in C programs.
__release_buffer__
: Allows you to free resources connected with a buffer.
With the addition of these methods, Python programs can now evaluate whether objects implement the buffer protocol, and type checkers can handle buffer types with more accuracy. This improvement allows for the tailored buffer classes in Python and improves compatibility with C-level APIs that use the buffer protocol.
Safe Refactoring: When an overridden function API changes, Python's type system lacks the means to identify call locations that need to be updated. Because type checkers cannot identify differences in overridden methods, refactoring code becomes dangerous.
Consider the following scenario: an inheritance hierarchy with a base class Parent and a class derived from the Child. If the overridden method on the superclass has been renamed or removed, type checkers only notify us to update call sites that directly interact with the base type. However, they must grasp that we will almost certainly rename the same function on child classes.
The @override decorator
is permitted anywhere a type checker considers a method to be a valid override. It includes standard methods, @property
, @staticmethod
, and @classmethod
.
Using this decorator, we explicitly indicate that a method must override some attribute in an ancestor class.
The decorator helps prevent subtle bugs caused by changes in overridden methods.
Let’s demonstrate how the @override
decorator works:
from typing import override
class Parent:
def foo(self, x: int) -> int:
return x
class Child(Parent):
@override
def foo(self, x: int) -> int:
return x + 1
def parent_callsite(parent: Parent) -> None:
parent.foo(1)
def child_callsite(child: Child) -> None:
child.foo(1) # Type checker ensures correct override
In this case, if we rename the overridden method in Parent by accident, the type checker will report an issue at the child_callsite, averting potential bugs. The @override decorator improves code safety during refactoring and assures subclass compatibility.
Python 3.12 expands the lexicon of the programming language, allowing programmers to articulate logic with elegance previously unattainable. Since various limits on f-strings have been lifted, the language changes have focused on ease of use and more flexibility. The new sort boundary linguistic structure and type explanation enhance the ergonomics of utilizing static kind checkers with nonexclusive sorts, type assumed names, and many more to explore!
Python 3.12 was made available on October 2nd, 2023. Please see the