paint-brush
What's New in Python 3.12?by@thisisjessintech
2,945 reads
2,945 reads

What's New in Python 3.12?

by Jess in TechNovember 17th, 2023
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

Python 3.12 is the most current stable release of the Python programming language, including a mix of language and standard library updates.
featured image - What's New in Python 3.12?
Jess in Tech HackerNoon profile picture

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.


New Type Annotation Syntax for Generic Classes

One of the new features in Python 3.12 is the new type annotation syntax for generic classes, described in PEP 695. This syntax allows you to express type parameters for generic classes like list, dict, tuple, and so on using square brackets instead of parentheses. For example, instead of writing list(int), you can write list[int]. It makes the type annotations more consistent with the syntax for indexing and slicing.


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.


More Flexible F-String Parsing

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.


Faster Python: More Specializations and Inlined Comprehensions

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 PEP 692, is another efficiency enhancement in Python 3.12. Instead of constructing a distinct function object for each comprehension, the Python interpreter can inline the code for list, set, and dict comprehensions. It decreases the overhead of constructing and invoking a function object and increases comprehension efficiency.


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.


TypedDict for more precise kwargs typing.

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.


PEP 692 has further information. This feature improves Python's type system and gives developers superb static analysis and code quality.


Enhancing Buffer Protocol Accessibility in Python

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.


PEP 688 offers two unique techniques at the Python level:


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.


PEP 688 bridges the gap between C-level access to the buffer protocol and Python-level introspection, making working with binary data effectively and reliably in Python 3.12 more straightforward.


Override Decorator for Static Typing

PEP 698 proposes that the Python-type system include a @override decorator. This decorator indicates that a method is overriding a method in a superclass. This innovation, inspired by comparable techniques in Java, C++, TypeScript, Kotlin, Scala, Swift, and C#, attempts to improve static type checking and discover potential errors when base class functions change.


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.


Summary

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 official documentation for more information on all of the new features in Python 3.12.