Skip to content
Home
Python 3.11: Pattern Matching, Error Groups, Performance

Python 3.11: Pattern Matching, Error Groups, Performance

Python Python 8 min read 1605 words Beginner ExcellentWiki Editorial Team

Python 3.11, released in October 2022, was described by the CPython core team as “the fastest Python yet.” The release delivered major performance improvements — up to 60% faster than Python 3.10 on benchmark suites — alongside significant new language features including refined structural pattern matching, exception groups with except*, zero-cost exception handling, improved error messages, and asyncio.TaskGroup for structured concurrency. This guide covers each major feature with practical examples and migration considerations.

Faster CPython: The Performance Revolution

The headline improvement in Python 3.11 is the “Faster CPython” project led by Mark Shannon, which rewrote critical interpreter internals to achieve substantial speedups without breaking backward compatibility. The project aims for a 5x speedup over Python 3.10 by Python 3.13, with 3.11 delivering the first major installment.

On the pyperformance benchmark suite, Python 3.11 achieves a geometric mean improvement of approximately 25% over Python 3.10. Individual benchmarks show improvements from 10% to over 60%. Computational workloads — numerical computation, text processing, data parsing — show the largest gains.

The performance improvements are achieved through several mechanisms: adaptive bytecode specialization that caches type information at runtime, optimized frame creation and function calls, and the “zero-cost” exception handling model. These changes are transparent to Python code — existing programs run faster without modification. PEP 659 provides the full specification for the adaptive interpreter.

Adaptive Bytecode Specialization

CPython 3.11 introduces adaptive bytecode. The interpreter starts with generic bytecode and, as it observes type patterns at runtime, replaces generic instructions with type-specialized versions. For example, a LOAD_ATTR instruction that repeatedly accesses attributes on dict instances is specialized to the dict-specific version, eliminating repeated type checks.

Each specialization includes an inline cache that stores the observed type. The next execution checks the cache first — if the type matches, execution proceeds at full speed. If the type changes, the specialization is deoptimized back to the generic version and may specialize to the new type. This adaptive mechanism is what makes the speedups transparent — code that exhibits stable type patterns benefits most, while highly polymorphic code falls back to the generic path.

The CPython developer guide’s Faster CPython section explains that function and method calls are approximately 10-20% faster in Python 3.11. Frame creation is optimized to avoid allocating Python objects for the frame when possible. Recursive function calls benefit substantially from this optimization.

Zero-Cost Exceptions

In Python 3.10 and earlier, setting up a try/except block had runtime overhead even when no exception was raised. The interpreter pushed exception handlers onto a stack on every try block entry. In Python 3.11, exception handling tables are moved to a side table in the code object. When no exception is raised, the interpreter never consults this table — the try block executes as if there were no exception handling.

Only when an exception is actually raised does the interpreter consult the side table to find the handler. This makes the “ask for forgiveness, not permission” pattern competitive with “look before you leap” in performance. As covered in Real Python’s Python 3.11 guide, this change has significant implications for how Python developers structure error handling in performance-critical code paths.

Structural Pattern Matching

Structural pattern matching, introduced in Python 3.10 as a provisional feature, is fully stabilized in Python 3.11. The match statement provides destructuring and conditional matching against data structures that previously required chains of isinstance() checks and attribute access.

Pattern matching in Python draws inspiration from Scala, Elixir, and Haskell, but adapts the concept to Python’s dynamic type system. A match statement evaluates a subject expression and attempts to match it against one or more patterns, executing the body of the first matching case. PEP 634 specifies the full grammar and semantics.

Literal patterns match specific values:

def http_status(code):
    match code:
        case 200:
            return "OK"
        case 201:
            return "Created"
        case 404:
            return "Not Found"
        case 500 | 502 | 503:
            return "Server Error"
        case _:
            return "Unknown"

Capture patterns bind matched values to variables. Guard expressions add conditional logic:

def process_coord(coord):
    match coord:
        case (0, 0):
            return "Origin"
        case (x, 0):
            return f"On X axis at {x}"
        case (0, y):
            return f"On Y axis at {y}"
        case (x, y) if x == y:
            return f"On diagonal at {x},{y}"
        case (x, y):
            return f"At {x},{y}"

Class patterns match objects by type and destructure attributes. This is particularly useful with dataclasses:

from dataclasses import dataclass

@dataclass
class Command:
    action: str
    payload: dict

def execute(cmd):
    match cmd:
        case Command("send", {"to": to, "body": body}):
            return send_message(to, body)
        case Command("delete", {"id": id_}):
            return delete_message(id_)
        case _:
            raise ValueError(f"Unknown command: {cmd}")

Mapping patterns match dictionaries by key patterns. This is useful for handling API responses or configuration dictionaries without verbose key checks. The wildcard pattern _ matches any value but does not bind. It is used for default cases and for ignoring specific positions in sequence patterns.

Pattern matching excels in several domains: processing tree-structured data (AST nodes, JSON responses), command dispatch, configuration parsing, and any code that requires chains of isinstance() checks or dictionary key existence tests. The Python documentation for the match statement provides additional examples.

Exception Groups and except*

Exception groups, proposed in PEP 654, allow raising and catching multiple unrelated exceptions simultaneously. This is particularly valuable for concurrent and parallel code where multiple operations can fail independently.

An ExceptionGroup wraps multiple exceptions:

def validate_batch(items):
    errors = []
    for i, item in enumerate(items):
        if not isinstance(item.get("name"), str):
            errors.append(ValueError(f"Item {i}: name must be a string"))
        if not isinstance(item.get("price"), (int, float)):
            errors.append(TypeError(f"Item {i}: price must be numeric"))
    if errors:
        raise ExceptionGroup("Validation failed", errors)

The except* syntax catches specific exception types within a group:

try:
    validate_batch(items)
except* ValueError as eg:
    print(f"Value errors: {eg.exceptions}")
except* TypeError as eg:
    print(f"Type errors: {eg.exceptions}")

Each except* clause catches only matching exceptions from the group. Unmatched exceptions propagate to the caller. If no except* matches any exception in the group, the original ExceptionGroup propagates unchanged. This design makes exception groups suitable for scenarios where some errors are recoverable and others are not.

Exception groups interact naturally with asyncio.TaskGroup and asyncio.gather(). When multiple tasks in a TaskGroup raise exceptions, they are collected into a single ExceptionGroup and re-raised in the parent coroutine. This is a major improvement over asyncio.gather() which returns exceptions as results requiring manual checking.

Better Error Messages

Python 3.11 improved traceback display with more precise error location information. When an exception occurs, the traceback now points to the exact subexpression that caused the failure, not just the line number:

data = {"items": [{"id": 1}, {"id": 2}]}
result = data["items"][3]["id"]
# Traceback (most recent call last):
#   File "example.py", line 2, in <module>
#     result = data["items"][3]["id"]
#             ~~~~~~~~~~~~~~^^^
# IndexError: list index out of range

The ~^^^ markers highlight data["items"][3] as the failing expression. This significantly speeds debugging, especially for complex nested expressions where the previous traceback would only show the line number, requiring manual inspection to identify which index access failed.

The error messages for common mistakes have also been improved. math.sqrt("hello") now produces TypeError: must be real number, not str instead of the less informative TypeError: a float is required. These improvements were guided by PEP 657 and are particularly helpful for beginners learning Python.

TaskGroup for Structured Concurrency

Python 3.11 added asyncio.TaskGroup for structured concurrency, implementing the pattern proposed in PEP 654. TaskGroup provides scoped concurrent task execution with automatic exception collection:

async def fetch_urls(urls):
    async with asyncio.TaskGroup() as tg:
        tasks = {url: tg.create_task(fetch(url)) for url in urls}
    # All tasks completed successfully at this point
    return {url: task.result() for url, task in tasks.items()}

If any task raises an exception, all sibling tasks are cancelled, and the exception (or ExceptionGroup for multiple failures) is raised at the async with statement. This prevents dangling tasks when one operation fails and ensures the group scope cannot exit with unhandled exceptions. TaskGroup replaces the common but error-prone pattern of asyncio.gather() with return_exceptions=True followed by manual exception checking.

Migration from Python 3.10

Migration from Python 3.10 is straightforward for most codebases. The Python 3.11 changelog lists a small number of removals and deprecations, primarily in the standard library. The distutils package is removed (replaced by setuptools), and asyncio.get_event_loop() now emits deprecation warnings in certain configurations.

Most third-party packages added Python 3.11 support within months of the release. The Python 3.11 What’s New document provides a complete migration checklist.

FAQ

How much faster is Python 3.11 than Python 3.10?

The pyperformance benchmark suite shows a geometric mean improvement of approximately 25% over Python 3.10. Individual workloads vary from 10% to over 60%. Computational and numerical workloads see the largest gains.

Is Python 3.11 backward compatible with Python 3.10?

Almost entirely. The main breaking change is the removal of the deprecated distutils module. A small number of standard library deprecations may produce warnings but do not break existing code.

What is structural pattern matching best used for?

Pattern matching excels at processing tree-structured data (AST nodes, JSON responses), command dispatch, configuration parsing, and any code that would require chains of isinstance() checks or dictionary key existence tests.

Do I need to change my code to benefit from Python 3.11 performance?

No. The Faster CPython optimizations are entirely transparent. Existing code runs faster without modification. No special annotations or API changes are required.

How do Task Groups differ from asyncio.gather()?

TaskGroup provides structured concurrency with automatic exception collection into ExceptionGroups and sibling task cancellation on failure. asyncio.gather() requires manual exception handling and does not cancel other tasks when one fails.


Related: Learn about Python concurrency patterns for working with asyncio and TaskGroups. Study Python design patterns for leveraging pattern matching in software design. Explore Python dataclasses for the class structures that pair well with pattern matching.

Section: Python 1605 words 8 min read Beginner 756 articles in section Report inaccuracy Back to top