Overview

Python 3.10, released on October 4, 2021, is one of the most anticipated releases in recent years. It introduces structural pattern matching (match/case), a feature inspired by functional languages that significantly enriches the language's expressiveness.

Beyond pattern matching, this release significantly improves error messages, allows parenthesized context managers, and strengthens the typing system with TypeAlias and ParamSpec.

Major Features

Structural pattern matching (PEP 634/635/636)

Structural pattern matching is the headline feature of Python 3.10. The match/case statement allows comparing a value against a series of patterns, with variable capture, guard conditions, and data destructuring. It is far more powerful than a simple switch/case from other languages.

python
# Example 1: command parser
def execute_command(command):
    """Interpret a text command."""
    match command.split():
        case ["quit"]:
            print("Goodbye!")
            return False
        case ["help"]:
            print("Commands: quit, help, open <file>, search <pattern> [in <file>]")
        case ["open", filename]:
            print(f"Opening {filename}")
        case ["search", pattern]:
            print(f"Searching for '{pattern}' in all files")
        case ["search", pattern, "in", filename]:
            print(f"Searching for '{pattern}' in {filename}")
        case _:
            print(f"Unknown command: {command}")
    return True

execute_command("open report.txt")
# Opening report.txt
execute_command("search error in journal.log")
# Searching for 'error' in journal.log
python
# Example 2: processing structured JSON data
def process_event(event):
    """Process an event based on its structure."""
    match event:
        case {"type": "login", "user": name}:
            print(f"Login by {name}")
        case {"type": "purchase", "product": product, "quantity": q} if q > 10:
            print(f"Bulk purchase: {q}x {product}")
        case {"type": "purchase", "product": product, "quantity": q}:
            print(f"Purchase: {q}x {product}")
        case {"type": "error", "code": code, "message": msg}:
            print(f"Error {code}: {msg}")
        case _:
            print(f"Unhandled event: {event}")

process_event({"type": "login", "user": "Alice"})
# Login by Alice
process_event({"type": "purchase", "product": "keyboard", "quantity": 25})
# Bulk purchase: 25x keyboard
process_event({"type": "purchase", "product": "mouse", "quantity": 3})
# Purchase: 3x mouse
python
# Example 3: pattern matching on classes
from dataclasses import dataclass

@dataclass
class Point:
    x: float
    y: float

@dataclass
class Circle:
    center: Point
    radius: float

@dataclass
class Rectangle:
    origin: Point
    width: float
    height: float

def describe_shape(shape):
    """Describe a geometric shape."""
    match shape:
        case Circle(center=Point(x=0, y=0), radius=r):
            print(f"Circle centered at origin, radius {r}")
        case Circle(center=Point(x=cx, y=cy), radius=r) if r > 100:
            print(f"Large circle at ({cx}, {cy}), radius {r}")
        case Circle(center=c, radius=r):
            print(f"Circle at ({c.x}, {c.y}), radius {r}")
        case Rectangle(width=w, height=h) if w == h:
            print(f"Square with side {w}")
        case Rectangle(width=w, height=h):
            print(f"Rectangle {w}x{h}")

describe_shape(Circle(Point(0, 0), 5))
# Circle centered at origin, radius 5
describe_shape(Rectangle(Point(1, 2), 10, 10))
# Square with side 10

Parenthesized context managers (PEP 617)

Thanks to the new PEG parser introduced in Python 3.9, it is now possible to use parentheses in with statements to spread multiple context managers across several lines in a readable way.

python
# Before Python 3.10: backslashes required
# with open('source.txt') as src, \
#      open('dest.txt', 'w') as dst:
#     dst.write(src.read())

# Python 3.10: parentheses allowed
with (
    open('source.txt') as src,
    open('dest.txt', 'w') as dst,
):
    dst.write(src.read())

# Very useful for multiple files
with (
    open('config.json') as config,
    open('data.csv') as data,
    open('report.txt', 'w') as report,
):
    # Processing using all three files
    pass

Better error messages

Python 3.10 significantly improves error messages. SyntaxError exceptions now indicate the problem location more precisely, and messages are more explicit. Unclosed parentheses, brackets, and braces are better reported, and attribute or name errors include suggestions.

python
# Unclosed parenthesis error:
# result = (1 + 2
#           ^
# SyntaxError: '(' was never closed

# Missing colon in if statement:
# if x > 0
#         ^
# SyntaxError: expected ':'

# Name error suggestion:
# number = 42
# print(nubmer)
# NameError: name 'nubmer' is not defined. Did you mean: 'number'?

# Attribute error suggestion:
# import collections
# collections.ordereddict
# AttributeError: module 'collections' has no attribute 'ordereddict'.
# Did you mean: 'OrderedDict'?

TypeAlias and ParamSpec (PEP 613, PEP 612)

TypeAlias lets you explicitly declare that a variable is a type alias, removing ambiguity with a simple assignment. ParamSpec allows typing decorators that preserve the decorated function's signature.

python
from typing import TypeAlias, ParamSpec, Callable, TypeVar
import functools
import time

# TypeAlias: declare an explicit type alias
Coordinates: TypeAlias = tuple[float, float]
Matrix: TypeAlias = list[list[float]]
Result: TypeAlias = dict[str, list[int]]

def distance(a: Coordinates, b: Coordinates) -> float:
    """Compute the distance between two points."""
    return ((a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2) ** 0.5

print(distance((0, 0), (3, 4)))  # 5.0

# ParamSpec: type a decorator that preserves the signature
P = ParamSpec('P')
T = TypeVar('T')

def stopwatch(func: Callable[P, T]) -> Callable[P, T]:
    """Measure a function's execution time."""
    @functools.wraps(func)
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
        start = time.perf_counter()
        result = func(*args, **kwargs)
        duration = time.perf_counter() - start
        print(f"{func.__name__}: {duration:.4f}s")
        return result
    return wrapper

@stopwatch
def compute_sum(n: int) -> int:
    return sum(range(n))

result = compute_sum(1_000_000)
# compute_sum: 0.0312s

Minor Improvements

  • int.bit_count() returns the number of set bits in the binary representation of an integer.
  • zip() gains a strict parameter to verify that iterables have the same length (PEP 618).
  • dataclasses support fields with slots=True and kw_only=True.
  • int, str, and bytes types gain performance improvements.
  • The statistics module gains new correlation and regression functions.

Deprecations and Removals

Notable changes in this category:

  • The typing module's type aliases (typing.Dict, typing.List, etc.) are deprecated in favor of built-in types.
  • The distutils module is deprecated and scheduled for removal in Python 3.12.
  • Non-text encodings in str.encode() are deprecated.

Sources