Python Match Case: Structural Pattern Matching Guide

Structural pattern matching, introduced in Python 3.10 via PEP 634, provides a powerful and expressive mechanism to evaluate expressions against specific patterns, extract data, and execute conditional logic based on an object's structure. This article explains how the match and case statements operate, exploring basic literal matching, sequence and mapping destructuring, class instance extraction, and pattern guards.

The Basic Syntax

At its core, structural pattern matching compares a subject expression against one or more patterns defined in case blocks. Unlike standard switch/case statements found in languages like C or JavaScript, Python's implementation matches both values and data shapes.

def process_command(command: str) -> None:
    match command:
        case "start":
            print("System starting...")
        case "stop":
            print("System stopping...")
        case _:
            print(f"Unknown command: {command}")

The underscore _ acts as a wildcard pattern that matches any value, functioning as the default fallback branch. Unlike some languages, execution does not "fall through" from one case to the next; once a pattern succeeds, its block executes, and matching terminates.

Sequence Destructuring

Structural pattern matching shines when unpacking sequences such as lists and tuples. You can check both the length of a sequence and bind its individual elements to variables.

def process_coordinates(point: tuple | list) -> None:
    match point:
        case (0, 0):
            print("Origin")
        case (0, y):
            print(f"On the Y-axis at {y}")
        case (x, 0):
            print(f"On the X-axis at {x}")
        case (x, y):
            print(f"Point at ({x}, {y})")
        case (x, y, *rest):
            print(f"3D+ Point starting at ({x}, {y}) with extra dimensions: {rest}")
        case _:
            print("Not a valid coordinate")

Variable names inside a pattern act as capture targets. If the structure matches, Python assigns the corresponding parts of the subject to those variables.

Mapping Destructuring

Patterns can match dictionary keys and bind their values. A dictionary pattern matches even if the dictionary contains additional keys not specified in the case, checking only for the presence of the specified keys.

def handle_event(event: dict) -> None:
    match event:
        case {"type": "click", "x": x, "y": y}:
            print(f"Click at position ({x}, {y})")
        case {"type": "keypress", "key": key}:
            print(f"Key pressed: {key}")
        case _:
            print("Unrecognized event structure")

To capture extra key-value pairs, the **rest syntax can be used.

Class and Object Matching

You can match against class instances by specifying the class name followed by positional or keyword patterns. This inspects object attributes directly.

class Point:
    def __init__(self, x: int, y: int):
        self.x = x
        self.y = y

def inspect_point(obj: object) -> None:
    match obj:
        case Point(x=0, y=0):
            print("Point is at the origin")
        case Point(x=x, y=y):
            print(f"Point instance at ({x}, {y})")
        case _:
            print("Not a Point instance")

If a class defines __match_args__, positional arguments can be used directly within the pattern without naming the attributes explicitly.

Pattern Guards and Combined Patterns

Patterns can be combined using the | (OR) operator, and they can include conditional expressions called "guards" using an if clause.

def validate_response(status: int, payload: dict) -> None:
    match status, payload:
        case (200 | 201, {"data": data}):
            print(f"Success with data: {data}")
        case (400 | 404 | 500, {"error": error}) if len(error) > 0:
            print(f"Failed with detailed error: {error}")
        case (status_code, _):
            print(f"Unhandled status: {status_code}")

A guard evaluates only after the structural pattern matches. If the guard evaluates to False, matching continues to the next case.

Key Differences from if/elif/else

Structural pattern matching is not a direct replacement for simple conditional chains. Use match/case when:

  1. Destructuring is required: When you need to unpack values from sequences, dictionaries, or objects while verifying their types and shapes.
  2. Handling polymorphic data: When managing varying input formats like parsed JSON payloads or abstract syntax trees.
  3. Readability is improved: When deeply nested isinstance() checks and dictionary lookups clutter the control flow.