Many developers, including myself, stuck with Python 3.7 until its end-of-life (EOL), preferring to “not fix what isn’t broken”. However, as I upgraded, I realized I was missing out on a host of new features, performance improvements, and useful updates. Switching from Python 3.7 to 3.12 introduces a host of enhancements and changes that can significantly streamline our coding experience and boost productivity. Here’s a summary of the notable updates from Python 3.7 to 3.12.


Python 3.7

1. Data Classes

The dataclasses module significantly reduces boilerplate code for classes that are mainly used to store data. Before Python 3.7, you had to manually write methods like __init__, __repr__, and __eq__ to initialize, represent, and compare instances. Data classes automate this process.

Usage Example:

from dataclasses import dataclass
 
@dataclass
class Point:
    x: int
    y: int
 
p = Point(1, 2)
print(p)  # Output: Point(x=1, y=2)

By using @dataclass, Python automatically generates the __init__, __repr__, and __eq__ methods, reducing repetitive code and improving readability.

2. Postponed Evaluation of Annotations

Python 3.7 introduced postponed evaluation of type annotations, storing them as strings rather than evaluating them immediately. This change allows for forward references—referring to classes that haven’t been defined yet—and can improve performance in large codebases by delaying the evaluation of annotations. - PEP 563

How to Enable: Add this import at the top of your file:

from __future__ import annotations

Example:

from __future__ import annotations
 
class Node:
    def __init__(self, value: int, next: Node = None):
        self.value = value
        self.next = next

With this feature, you can reference classes that haven’t been defined yet, which enhances flexibility and compatibility in your code.

3. breakpoint() Function

The breakpoint() function simplifies debugging by allowing us to enter the debugger without needing to manually import pdb and call pdb.set_trace().

Usage Example:

def example_function():
    x = 10
    breakpoint()  # Execution stops here and enters the debugger
    y = 20
    return x + y

This feature makes debugging more straightforward and reduces setup time.

4. Context Variables

The contextvars module was introduced to manage context-local state, which is crucial in asynchronous programming. It ensures that each task has its own state, avoiding conflicts in concurrent executions.

Usage Example:

import contextvars
import asyncio
 
request_id = contextvars.ContextVar('request_id')
 
async def handle_request(req_id):
    request_id.set(req_id)
    print(f'Handling request with ID: {request_id.get()}')
 
async def main():
    await asyncio.gather(
        handle_request('req1'),
        handle_request('req2'),
    )
 
asyncio.run(main())

Output:

Handling request with ID: req1
Handling request with ID: req2

Context variables help maintain separate states across different asynchronous tasks, making them indispensable for concurrency safety.


Python 3.8

1. Walrus Operator

The walrus operator (:=) allows you to assign a value to a variable within an expression. This reduces redundant code by combining variable assignment and usage in a single statement.

Usage Example:

# Traditional way
n = len(my_list)
if n > 10:
    print(f"List is too long ({n} elements).")
 
# Using walrus operator
if (n := len(my_list)) > 10:
    print(f"List is too long ({n} elements).")

In the walrus example, n is assigned the length of my_list and immediately used in the condition, making the code more concise.

2. Positional-Only Parameters

We can now specify parameters that must be provided positionally (without using keyword arguments). This is done using the / symbol in function definitions, allowing for more controlled API designs.

Usage Example:

def show_info(a, b, /, c, d):
    print(a, b, c, d)
 
# Valid call: a and b are positional, c and d can be either
show_info(1, 2, c=3, d=4)
 
# Invalid call: a and b must be positional
show_info(a=1, b=2, c=3, d=4)  # This will raise an error

Here, a and b must be given in order, while c and d can be given in any order or as keyword arguments.

3. f-strings Enhancements

f-strings now support = to show both the variable name and its value. This makes debugging easier by providing more context.

Usage Example:

value = 42
print(f"{value=}")  # Output: value=42

This prints the variable name and its value, making it clearer what’s being printed and why.

4. continue Statements in finally Blocks

Python 3.8 allows using continue statements in finally blocks, which was not allowed before. This helps manage control flow better in loops.

Usage Example:

items = [1, 2, 3, 4, 5]
 
for item in items:
    try:
        print(f"Processing item {item}")
        if item == 3:
            raise ValueError("Error processing item 3")
    finally:
        if item == 3:
            continue  # Skip to the next iteration if there's an error
        print("Cleaning up after item", item)
 
print("Finished processing all items")

In this example, if an error occurs while processing item 3, the continue statement in the finally block ensures that the cleanup code for item 3 is skipped, and the loop moves to the next item which was not possible earlier.


Python 3.9

1. Dictionary Merging and Updating

Python 3.9 introduced new operators (| and |=) for merging and updating dictionaries, making it more intuitive to combine dictionary data (PEP 584).

Usage Example:

d1 = {'a': 1, 'b': 2}
d2 = {'b': 3, 'c': 4}
 
# Merge two dictionaries using |
merged = d1 | d2  # {'a': 1, 'b': 3, 'c': 4}
print(merged)
 
# Update the first dictionary in place using |=
d1 |= d2  # d1 is now {'a': 1, 'b': 3, 'c': 4}
print(d1)

In this example, d1 | d2 creates a new dictionary combining d1 and d2. If there are duplicate keys, the value from the second dictionary (d2) will be used. The |= operator updates d1 with the contents of d2 directly.

2. Type Hinting Generics

With Python 3.9, you can use built-in types like list, dict, and tuple directly in type hints without needing to import them from the typing module (PEP 585).

Usage Example:

def process_items(items: list[int]) -> None:
    for item in items:
        print(item)
 
process_items([1, 2, 3])  # Output: 1 2 3

In this function, items is expected to be a list of integers. The list[int] syntax is now valid without any additional imports, making code easier to read and write.

3. New String Methods: .removeprefix() and .removesuffix()

Python 3.9 added two new string methods to simplify removing specific prefixes or suffixes from strings.

Usage Example:

filename = "example.txt"
 
# Remove the suffix '.txt'
name_without_extension = filename.removesuffix(".txt")
print(name_without_extension) # Output: 'example'

These methods make it easier to manipulate strings without needing complex slicing or checking methods.


Python 3.10

1. Structural Pattern Matching

Python 3.10 introduced match and case statements, providing a powerful way to handle complex conditional logic by matching specific patterns (PEP 634).

Usage Example:

def http_status(status):
    match status:
        case 200:
            return "OK"
        case 404:
            return "Not Found"
        case _:
            return "Error"
 
print(http_status(200))  # Output: OK
print(http_status(404))  # Output: Not Found
print(http_status(500))  # Output: Error

Here, match checks the status variable against different cases, similar to a switch statement in other languages, but more flexible and powerful. Read more detail usage in Python Match Case Statement.

2. Parenthesized Context Managers

You can now use multiple context managers more cleanly by enclosing them in parentheses across multiple lines (PEP 343).

Usage Example:

with (
    open("file1.txt") as f1,
    open("file2.txt") as f2
):
    data1 = f1.read()
    data2 = f2.read()

This enhancement improves code readability when handling multiple resources, especially when lines are lengthy.

3. Precise Error Messages

Python 3.10 improved error messages by providing more specific information about where and why an error occurred.

Example of an Improved Error Message:

def example(a, b):
    return a + b
 
example(5, "test")

Error Message:

# Old Error Message
TypeError: unsupported operand type(s) for +: 'int' and 'str'

# New Error Message in Python 3.10
TypeError: can only concatenate str (not "int") to str

The new error message clearly indicates the operation causing the issue, aiding in faster debugging.


Python 3.11

1. Exception Groups and except* Syntax

Python 3.11 introduced exception groups, which allow multiple exceptions to be raised and the except* syntax to handle multiple exceptions in a single block, making it easier to manage concurrent tasks and handle different error types (PEP 654).

Usage Example:

try:
    # Simulate multiple exceptions
    raise ExceptionGroup("Multiple errors", [ValueError("Value error"), TypeError("Type error")])
except* ValueError as e:
    print(f"Handled value error: {e}")
except* TypeError as e:
    print(f"Handled type error: {e}")

This allows handling specific types of exceptions in more granular ways, improving error handling in asynchronous or concurrent code.

2. Faster Python: CPython Optimizations

Python 3.11 includes significant optimizations to CPython, resulting in performance improvements ranging from 10-60% for various workloads. This optimization focuses on reducing execution overhead and improving runtime speed.

3. Variadic Generics

Variadic generics allow defining generics that can take an arbitrary number of type parameters, making it easier to work with types that require flexible parameter counts (PEP 646).

Usage Example:

from typing import TypeVarTuple, Generic
 
T = TypeVarTuple('T')
 
class MyGenericClass(Generic[*T]):
    def __init__(self, *args: *T):
        self.args = args
 
instance = MyGenericClass(1, 'a', 3.14)
print(instance.args)  # Output: (1, 'a', 3.14)

This example shows a class that can handle arguments of any type and any number, demonstrating the flexibility of variadic generics.

4. Fine-Grained Error Locations

Error messages and tracebacks in Python 3.11 now more accurately pinpoint the exact location of an error, making debugging easier and more precise.


Sure, I can help tighten up the content of your blog post without losing important information. Here’s a more concise version:


Python 3.12

1. Enhanced f-strings

Python 3.12 introduces several enhancements to f-strings, making them even more powerful.

String Literal Nesting

You can now nest string literals and f-strings:

things = ["apple", "banana", "cherry"]
result = f"These are the things: {', '.join(things)}"
print(result)  # Output: These are the things: apple, banana, cherry

Backslashes and New Lines in Expressions

Backslashes and new lines are now supported in f-string expressions:

a = ["hello", "world"]
result = f"{'\n'.join(a)}"
print(result)
 
x = 1
result = f"___{\n    x\n}___"
print(result)

Output:

hello
world

___
    1
___

Comments in Expressions

Comments can now be added within f-string expressions:

a = 1
result = f"___{
    a  # This is a comment
}___"
print(result)  # Output: ___1___

These updates make f-strings more versatile and readable.

2. TypedDict Improvements

Python 3.12 introduces typing.ReadOnly for TypedDict fields, enforcing immutability:

from typing import TypedDict, ReadOnly
 
class Movie(TypedDict):
    title: str
    year: int
 
class ImmutableMovie(Movie, total=False):
    title: ReadOnly[str]
 
m: ImmutableMovie = {"title": "Inception"}
# m['title'] = "Another"  # TypeError: "title" is read-only

The ReadOnly type hint ensures that specified fields remain unchanged after assignment, enhancing data structure integrity.


References