Structural Pattern Matching in Python: Master the match-case Statement
Introduction
In Python 3.10, a powerful new feature was introduced: structural pattern matching, implemented through the match-case
statement. If you are familiar with languages like C, C++, or JavaScript, you might have used switch-case
statements to handle multiple branching conditions. While Python’s match-case
superficially resembles switch-case
, it is far more advanced and expressive.
Structural pattern matching allows you to match the shape and contents of data structures directly in control flow statements. You can destructure complex nested data — such as lists, tuples, dictionaries, and even custom objects — and write clear, concise branching logic based on their structure and values.
This feature was proposed in PEP 634 and introduced as part of Python’s continuous effort to make code more readable and maintainable. It goes beyond simple value comparison, enabling elegant handling of structured data like JSON responses, abstract syntax trees (ASTs), and hierarchical objects.
Why should you use match-case
instead of traditional if-elif-else
blocks? Because it allows you to:
Express complex branching more clearly, especially with structured or nested data.
Avoid repetitive and verbose conditions.
Write declarative code that directly mirrors the data structure you are working with.
In this post, you will learn the syntax and capabilities of match-case
, explore different types of patterns you can use, and see practical examples that demonstrate when and how to take advantage of this modern Python feature.
Let’s begin by understanding how match-case
works and why it has become an essential tool for writing clean and expressive Python code.
Prerequisites & Soft Keywords
Before you start using structural pattern matching with the match-case
statement, there are a couple of important prerequisites to be aware of.
Python Version Requirement
Structural pattern matching was introduced in Python 3.10. This means that if you are using an earlier version of Python, such as 3.9 or below, the match
and case
statements will not be recognized and will result in a syntax error.
To check your current Python version, you can run:
python --version
If your version is older than 3.10, consider upgrading to a newer Python version to take advantage of this and many other modern features.
Soft Keywords
In Python, some words are reserved — they cannot be used as variable names or identifiers in any context (e.g., if
, else
, for
, while
, etc.). However, match
and case
are implemented as soft keywords.
What does this mean?
They behave like keywords only when used in the correct syntactic context (inside a
match
block).Outside of a
match-case
block, you can still usematch
orcase
as variable names, function names, or class names without any issues.
Example:
match = "hello" # perfectly valid outside a match-case block
def case():
print("This is a function named case.") # also valid
But inside a match
block:
match value:
case "hello":
print("Matched hello!")
Here, both match
and case
act as control flow constructs.
This design choice ensures backward compatibility with existing code and allows gradual adoption of structural pattern matching in Python codebases.
Basic Syntax
Here’s a clean and focused overview of the fundamental match-case
syntax in Python 3.10+, without diving into specific pattern types just yet:
Structure of a match-case
Block
match subject:
case pattern_1:
action_1
case pattern_2:
action_2
case _:
default_action
match subject:
Python evaluates thesubject
expression once.Sequential
case
checks: Eachcase
is tested in order, and the first matching pattern triggers its corresponding action. No further cases are evaluated after a match.Wildcard
_
: Acts as a default—matching anything not caught by previous cases.
Key Characteristics
Single evaluation of
subject
, improving both performance and clarity.No fall-through, unlike C-style
switch
; after a case matches, processing stops inside the block.Declarative style, where the structure of your conditions mirrors the code flow—making it easier to read and maintain .
Simple Usage Example
def check_status(code):
match code:
case 200:
return "OK"
case 404:
return "Not Found"
case _:
return "Other Status"
This behaves like a standard
switch
for simple integer or string cases.The default wildcard case ensures unmatched values are still handled predictably.
Summary
match-case
offers a concise, structured approach to conditional logic.The wildcard
_
acts like anelse
.There’s no fall-through, enhancing readability and reducing errors.
Complex patterns are supported—but we'll explore those in the “Pattern Types” section next.
Pattern Types
Python’s structural pattern matching supports several pattern types. You can mix and match them to handle various data structures elegantly.
Wildcard Pattern
_
A fallback for any unmatched case.
def greet(code): match code: case 200: print("OK") case _: print("Other Status") greet(500) # Output: Other Status
500
doesn’t match 200, so the wildcard case runs.
Literal Patterns
Match exact values using
==
(oris
forNone
,True
,False
).def greet(code): match code: case 200: print("OK") case 404: print("Not Found") case _: print("Other Status") greet(200) # Output: OK greet(500) # Output: Other Status
200
matches the first case, so "OK" is printed.500
doesn’t match 200 or 404, so the wildcard case runs.
OR Patterns
Use
|
to match any one of several options.def check(error_code): match error_code: case 400 | 401 | 403: print("Client error") case _: print("Other") check(401) # Output: Client error check(500) # Output: Other
401
matches400 | 401 | 403
, so "Client error" is printed.
Sequence Patterns
Match and unpack lists or tuples with structural patterns.
def describe_point(pt): match pt: 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})") case _: print("Some other point") describe_point((0, 0)) # Origin describe_point((3, 0)) # On X-axis at 3 describe_point([5, 7]) # Point at (5, 7) describe_point([1, 2, 3]) # Some other point
The tuple
(3, 0)
hits the second case, bindingx = 3
.The tuple
(5, 7)
hits the second case, bindingx = 5
andy = 7
.
Mapping (Dictionary) Patterns
Match dicts by key and bind values.
def process_user(user): match user: case {"name": name, "age": age, "role": "admin"}: print(f"{name} is an admin aged {age}") case {"name": name, "age": age}: print(f"{name} is {age} years old") case _: print("Unknown user") process_user({"name": "Anupam", "age": 24, "role": "admin"}) # Output: Anupam is an admin aged 24 process_user({"name": "Bob", "age": 30}) # Output: Bob is 30 years old
Class Patterns
Work with objects, including dataclasses and namedtuples.
from dataclasses import dataclass @dataclass class Point: x: int y: int def handle_point(pt: Point): match pt: case Point(x, y): print(f"Point at ({x}, {y})") case _: print("Not a point") handle_point(Point(2, 3)) # Output: Point at (2, 3) handle_point((2, 3)) # Output: Not a point
The
Point(x, y)
pattern destructures the attributes directly.
Capture Patterns
Bind individual elements of a structure.
(See Sequence, Dictionary and Class examples above; each
x
,y
,age
variable binds dynamically.)As Patterns
Alias matched data for later use.
def analyze_list(lst): match lst: case [first, *rest] as full_list: print(f"First={first}, Full list={full_list}") case _: print("Not a list") analyze_list([1, 2, 3]) # Output: First=1, Full list=[1, 2, 3]
The
as full_list
keeps the entire list alongside matched parts.
Guard Clauses
Add conditions to refine matches.
def process_user(user): match user: case {"name": name, "age": age} if age < 18: print(f"{name} is a minor") case {"name": name, "age": age, "role": "admin"}: print(f"{name} is an admin aged {age}") case {"name": name, "age": age}: print(f"{name} is {age} years old") case _: print("Unknown user") process_user({"name": "Alice", "age": 15}) # Output: Alice is a minor process_user({"name": "Anupam", "age": 10, "role": "admin"}) # Output: Anupam is a minor process_user({"name": "Bob", "age": 42, "role": "admin"}) # Output: Bob is an admin aged 42 process_user({"name": "Charlie", "age": 30}) # Output: Charlie is 30 years old
These examples showcase how structural pattern matching lets you write clear, correct code—far more succinct than traditional if-elif
chains.
Limitations & Gotchas
Order-Sensitive Branch Reachability
Issue:
case
clauses are evaluated sequentially; earlier, broader patterns can shadow more specific ones.Example:
rows = [ {"success": True, "value": 200}, ] match rows[0]: case {"success": True, "value": _}: print("Matched general") case {"success": True, "value": 200}: print("Matched specific")
Output:
Matched general
The second case never runs due to the general pattern first.
Best Practice: Always place specific patterns before general ones, especially when they overlap.
Non‑Exhaustive Matching
Issue: Python doesn’t enforce exhaustive coverage. Missing cases can lead to unhandled scenarios silently.
Example:
from enum import Enum class C(Enum): A=1 B=2 C=3 def describe(c: C): match c: case C.A: return "A" case C.B: return "B" print(describe(C.C)) # Returns None, no exception
You won’t get an error or warning.
Best Practice: Always include a wildcard (
case _:
) or explicitly raise an exception for unknown cases.
Accidental Name Capture
Issue: Bare names in patterns are treated as new variables, not constants.
Example:
NOT_FOUND = 404 match 404: case NOT_FOUND: print("Matched?") print(NOT_FOUND) # Prints 404
This rebinding is unexpected.
Best Practice: Use qualified names (e.g.,
Status.NOT_FOUND
) orEnum
types to avoid capture.
Syntax Confusion: Duplicated Bindings
Issue: You cannot reuse the same variable name in overlapping subpatterns.
Example:
match (1, 2), (3, 4): case ((x, *y), (x, *z)): ...
Raises a syntax error due to conflicting
x
bindings.Best Practice: Use distinct variable names for each binding in nested structures.
Performance
Observation:
match-case
is implemented as a sequence of runtime checks—comparable toif-elif
, not optimized lookups.Benchmark: (Source)
match-case
: ~0.0044 sif-elif
: ~0.0043 sDictionary lookup: ~0.0008 s.
Best Practice: Favor
match-case
for expressive structural patterns, but use dicts or lookup tables for frequent literal comparisons.
No Fall‑Through
Observation: Unlike C-style
switch
, there’s no fall-through behavior.Example:
match x: case 1: print("One") case 2: print("Two")
You cannot intentionally fall through from case 1 to case 2.
Best Practice: Duplicate actions or refactor logic into shared functions if needed.
Misuse of Wildcard with Mapping Patterns
Issue: Using
**_
in dict patterns is disallowed.Example:
case {"age": age, **_}:
is invalid.Best Practice: Use
case {"age": age} as data:
if you need the full mapping.
Summary Table
Pitfall | Impact | Recommended Practice |
---|---|---|
Order-Sensitive Branching | Specific cases ignored | Order from specific → general |
Non-Exhaustive Matching | Silent failures | Add wildcard case or throw exceptions |
Name Capture Confusion | Variables unintentionally rebound | Use qualified names or Enums |
Duplicate Bindings | Syntax errors | Ensure distinct variable names |
Performance | Slower than dict lookups | Use |
No Fall-Through | Longer logic for shared behavior | Refactor shared code into functions |
Invalid Wildcard Usage | Syntax error | Use |
By understanding these limitations and caveats, you can confidently apply match-case
in situations where its expressive power outweighs potential pitfalls.
Conclusion
Structural pattern matching, introduced in Python 3.10 with the match-case
statement, brings a powerful and expressive new tool to the language. It allows developers to write cleaner, more readable code for complex branching logic—especially when working with structured data such as lists, tuples, dictionaries, and objects.
By understanding how to apply literal patterns, OR patterns, sequence patterns, mapping patterns, class patterns, and more advanced features like capture patterns and guard clauses, you can significantly simplify your control flow. Pattern matching is particularly effective when dealing with data transformations, protocol parsing, and situations where nested if-elif-else
chains would otherwise become cumbersome.
However, it is important to be mindful of its limitations and gotchas, such as order sensitivity, accidental name capture, and non-exhaustive matching. Used with care and clarity, match-case
can become an indispensable part of your Python programming toolkit.
As Python continues to evolve, pattern matching is poised to become a core idiom of modern Python code. By mastering it now, you prepare yourself to write more elegant and maintainable code for years to come.