Back to Blog
Python

Python match case: Structural Pattern Matching

python match case: Learn Python match case for structural pattern matching: syntax, patterns, guards, common pitfalls, and when to prefer it over if-elif.

structural pattern matchingPython 3.10control flowpattern matchingswitch statement
Illustration of Python structural pattern matching with match case branches leading to different outcomes.

Python match case, introduced in Python 3.10, provides a way to perform structural pattern matching on data. Unlike a traditional switch statement, which only compares values, match case can destructure and bind parts of the data, making it a powerful tool for working with complex data structures. This article explains the syntax, shows practical patterns, and highlights the pitfalls that trip up developers new to the feature.

The Core Syntax of Python match case

The basic structure of a match statement is straightforward: you match a subject against one or more case clauses, each containing a pattern and an optional block of code. When a pattern matches, the corresponding block runs, and the match statement ends. There is no fallthrough, so you do not need break statements.

command = "start" match command: case "start": print("Starting...") case "stop": print("Stopping...") case _: print("Unknown command")

The underscore _ acts as a wildcard and matches anything. It is often used as the default case. The subject can be any expression, and the patterns are evaluated in order. The first matching case wins.

Matching Literals and Capturing Values

Literal patterns match exact values, such as strings, numbers, and booleans. You can also capture the matched value into a variable by using a name without quotes. A capture pattern binds the subject to the variable name, which is useful when you need to use the value inside the block.

def describe(value): match value: case 0: return "zero" case 1: return "one" case n: return f"other number: {n}"

Here, n captures the value when it is not 0 or 1. The wildcard _ does not bind a name, while a capture pattern does. If you use the same name more than once in a case, the second occurrence becomes a comparison, not a binding, which can lead to surprising behavior.

Matching Sequences and Mappings

Sequence patterns let you match against lists, tuples, and other sequence types. You can specify the exact length or use * to capture the rest of the sequence. Mapping patterns work with dictionaries and check for the presence of keys.

point = (3, 4) match point: case (0, 0): print("Origin") case (x, 0): print(f"On x-axis at {x}") case (0, y): print(f"On y-axis at {y}") case (x, y): print(f"Point at ({x}, {y})")

For mappings, you can match specific keys and bind their values:

config = {"host": "localhost", "port": 8080} match config: case {"host": host, "port": port}: print(f"{{host}}:{port}") case {"host": host}: print(f"{host} (default port)")

The mapping pattern requires the specified keys to be present, but it ignores extra keys. To require an exact match, use **rest to capture the remaining items, but note that you cannot match a mapping with no extra keys without using a guard.

Using Class Patterns and Custom Objects

Class patterns allow you to match against the type and attributes of an object. This is especially useful for dataclasses and other structured types.

from dataclasses import dataclass @dataclass class Point: x: int y: int shape = Point(1, 2) match shape: case Point(x=0, y=0): print("Origin") case Point(x=x, y=y): print(f"Point at ({x}, {y})") case _: print("Not a point")

The class pattern checks the type and then matches the attributes as specified. You can use positional arguments if the class defines __match_args__, which dataclasses do by default. This makes the pattern concise and readable.

Adding Guards for Conditional Matching

A guard is an if clause that adds an extra condition to a case. The pattern must match first, and then the guard is evaluated. If the guard is false, the match continues to the next case.

match number: case n if n > 0: print(f"Positive: {n}") case n if n < 0: print(f"Negative: {n}") case _: print("Zero")

Guards are essential when you need to match a pattern but also enforce a condition that cannot be expressed in the pattern itself, such as a range check or a comparison between bound variables.

Combining Patterns with OR and AS

The OR pattern, written with |, lets you combine multiple patterns into one case. This is useful when different patterns should lead to the same action. The AS pattern, written with as, binds the entire matched subject to a name, which is helpful when you need the original value after destructuring.

match command: case "start" | "begin": print("Starting") case ("stop" | "end") as action: print(f"Stopping with {action}")

When using OR, each alternative must bind the same set of names; otherwise, the pattern is invalid. The AS pattern is often combined with class or sequence patterns to keep a reference to the whole object.

Common Pitfalls and How to Avoid Them

One common mistake is assuming match case behaves like a switch with fallthrough. It does not. Each case is independent, and only the first matching case executes. Another pitfall is using a capture pattern when you meant a literal. For example, case "x": matches the string "x", but case x: captures any value into x. If you forget quotes, you may get unexpected behavior.

Another issue is reusing a variable name inside a pattern. In a single case, a name can be bound only once. If you write case (x, x):, the second x is treated as a comparison with the first, not a binding. This often raises a SyntaxError or behaves unexpectedly. To match two equal values, use a guard: case (a, b) if a == b:.

Finally, remember that the subject is evaluated once, but the patterns are evaluated in order. If you have many cases, the runtime cost is linear in the number of cases, similar to an if-elif chain. There is no compiler optimization that turns match into a jump table.

When to Use match Instead of if-elif

Match case shines when you are working with data that has a structure, such as nested lists, dictionaries, or objects. It reduces boilerplate and makes the intent clear. For simple value comparisons, an if-elif chain is often more readable and does not require the extra indentation.

Use match when:

  • You need to destructure complex data in a single step.
  • You want to combine type checks and attribute access in one pattern.
  • You have multiple branches that depend on the shape of the data.

Stick with if-elif when:

  • You are only comparing against a few literals.
  • The conditions are not structural but involve arbitrary expressions.
  • You need to evaluate conditions that cannot be expressed as patterns, even with guards.

The decision is about clarity and maintainability. Match case can make code more declarative and easier to extend, but it is not always the best tool.

Performance and Maintainability Considerations

From a performance perspective, match case does not offer a speed advantage over if-elif. The Python interpreter compiles match to a series of checks, and each case is evaluated sequentially. The cost is proportional to the number of cases, so for performance-critical code with many branches, you might consider other dispatch mechanisms like dictionaries or functools.singledispatch. However, for typical application code, the difference is negligible.

Maintainability is where match case provides real value. It groups related patterns together and makes the data flow explicit. Adding a new case is straightforward, and the structure encourages you to handle all possible shapes of the data. That said, overusing match can make code harder to read if the patterns become too complex. Keep patterns simple and use guards sparingly. If a case requires a long guard, consider extracting the logic into a function.

Another consideration is compatibility. Match case requires Python 3.10 or later. If you are supporting older versions, you cannot use this syntax. In that case, you need to fall back to if-elif chains or use third-party libraries like attrs or dataclasses with manual dispatch. When upgrading, be aware that match is a soft keyword, so code that used match as an identifier may break in Python 3.10 and later.

python match case: Practical Usage and Code Examples | RYUSLOG DEV