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.
đź”— Quick Links
- Python 3.7: Data Classes, Postponed Evaluation of Annotations,
breakpoint()
Function, Context Variables- Python 3.8: Walrus Operator
:=
, Positional-Only Parameters,f-strings
Enhancements,continue
Statements infinally
Blocks- Python 3.9: Dictionary Merging and Updating, Type Hinting Generics, New String Methods:
.removeprefix()
and.removesuffix()
- Python 3.10: Structural Pattern Matching, Parenthesized Context Managers, Precise Error Messages
- Python 3.11: Exception Groups and
except*
Syntax, Faster Python: CPython Optimizations, Variadic Generics, Fine-Grained Error Locations- Python 3.12: Enhanced
f-strings
,TypedDict
Improvements
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.