Long if/elif chains checking the same variable against value after value get repetitive fast — and Python 3.10 introduced something genuinely more powerful than a simple fix for that. python match case, formally called structural pattern matching, doesn't just compare values — it can inspect the shape, type, and contents of data all in one place. This article covers the syntax from the ground up, including the parts that trip people up.
Introduction: Beyond if-elif Chains
match/case was introduced in Python 3.10 through PEP 634. It's often described as Python's answer to the switch statement found in languages like C, Java, or JavaScript — but that description undersells what it actually does.
The key distinction
A traditional switch statement (and a simple if/elif chain doing the same job) only compares a value against other values for equality. Python's match statement does something more ambitious: it performs structural pattern matching — meaning it can check not just a value, but the shape of a value: is this a tuple with exactly two elements? Does this dictionary have a "type" key with a specific value? Is this an instance of a particular class with certain attributes? All of that is expressible directly in the pattern itself.
Basic syntax
match subject:
case pattern1:
# runs if subject matches pattern1
case pattern2:
# runs if subject matches pattern2match evaluates the subject once, then checks it against each case pattern in order, running the first one that matches.
Basic Patterns: Literals, Wildcards, and Capture
Literal matching
The simplest form checks against specific literal values, much like a traditional switch statement would:
command = "help"
match command:
case "start":
print("Starting...")
case "stop":
print("Stopping...")
case "help":
print("Available commands: start, stop, help")The wildcard _
An underscore _ acts as a catch-all, matching anything — it's the equivalent of a default case in other languages:
match command:
case "start":
print("Starting...")
case "stop":
print("Stopping...")
case _:
print("Unknown command")One important detail: _ never binds a name. Unlike a normal variable pattern (covered next), matching against _ doesn't give you a usable variable afterward — it's purely a "match anything, don't bother capturing it" signal.
Capturing values directly in a pattern
If you use a plain lowercase name (not a literal, not _) as a pattern, it doesn't check for equality at all — it captures whatever the subject is into that name:
match command:
case "start":
print("Starting...")
case other:
print(f"Unrecognized command: {other}")Here, other isn't compared against anything — it always matches, and command's value gets bound to the name other for use inside that block. This is functionally similar to _, except it also gives you a usable variable.
Combining alternatives with |
You can match several literal alternatives in a single case using |:
match command:
case "Alice" | "Bob":
print("Recognized user")
case _:
print("Unknown user")Destructuring Sequences and Mappings
Sequence patterns
match can check a tuple or list against a specific shape, capturing individual elements along the way:
point = (0, 5)
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})")
# On the y-axis at 5Each pattern checks both the shape (does this have exactly two elements?) and specific positions (is the first element literally 0?) at the same time. The first pattern that matches both the structure and any literal values wins.
Mapping patterns
Dictionaries can be matched by key, capturing the corresponding values:
c {"type": "circle", "radius": 5}
match config:
case {"type": "circle", "radius": r}:
print(f"Circle with radius {r}")
case {"type": "square", "side": s}:
print(f"Square with side {s}")
case _:
print("Unknown shape")
# Circle with radius 5Notably, a mapping pattern doesn't require an exact match on the dictionary's full contents — extra keys in the actual dictionary that aren't mentioned in the pattern are simply ignored. The pattern only checks for the keys it explicitly names.
Why this is closer to unpacking with conditions
If you're familiar with Python's iterable unpacking (x, y = point, covered in the earlier article on assignment operators), sequence patterns will feel familiar — match essentially extends that same idea, but adds the ability to check specific values while unpacking, rather than unconditionally assigning everything. That's the fundamental difference from a traditional switch statement: it's not just "which value matches," it's "which shape, with which specific values in specific positions, matches."
Class Patterns and Guard Clauses
Matching against custom class instances
match can check whether a subject is an instance of a specific class, while simultaneously capturing its attributes:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
point = Point(0, 0)
match point:
case Point(x=0, y=0):
print("Point is at the origin")
case Point(x=0, y=y):
print(f"Point is on the y-axis at {y}")
case Point(x=x, y=y):
print(f"Point is at ({x}, {y})")
# Point is at the originThis checks both that point is a Point instance and that its x and y attributes match the specified values or get captured into new variables — all in a single, readable pattern.
Guard clauses with if
You can attach an additional condition to any case using if, for checks that go beyond what structural matching alone can express:
match point:
case Point(x=x, y=y) if x == y:
print("Point is on the diagonal")
case Point(x=x, y=y):
print(f"Point is at ({x}, {y})")The guard clause only runs — and the pattern only counts as a match — if both the structural pattern matches and the if condition afterward evaluates to True. If the guard fails, match moves on to try the next case, even though the structural part matched.
Practical example: a simple command dispatcher
def handle_event(event):
match event:
case {"type": "click", "x": x, "y": y}:
print(f"Click at ({x}, {y})")
case {"type": "keypress", "key": key} if key in ("Enter", "Escape"):
print(f"Special key pressed: {key}")
case {"type": "keypress", "key": key}:
print(f"Key pressed: {key}")
case _:
print("Unknown event")
handle_event({"type": "click", "x": 10, "y": 20})
# Click at (10, 20)This is a genuinely common real-world shape for match to shine in — dispatching behavior based on the structure of incoming data, such as parsed JSON, event objects, or messages from an external system.
Common Pitfalls and Best Practices
Case order matters
Just like elif chains, match evaluates case patterns from top to bottom and stops at the first one that matches. This means a broad capture pattern placed too early will silently shadow every case written after it:
match command:
case other: # this matches literally anything!
print(f"Got: {other}")
case "start": # this line is now unreachable — dead code
print("Starting...")Because other is a plain capture pattern (not a specific literal), it matches every possible value, meaning the "start" case underneath it can never be reached. Always order patterns from most specific to least specific, with wildcard or general capture patterns last — the exact same rule that applies to elif chains.
No exhaustiveness checking
Unlike some other languages with pattern matching, Python's match statement doesn't require you to cover every possible case, and it won't warn you if you don't. If none of the case patterns match and there's no _ wildcard, the match statement simply does nothing — no error, no output, execution just continues past it silently.
match command:
case "start":
print("Starting...")
case "stop":
print("Stopping...")
# If command is "restart", nothing happens at all — no error, no outputIf you want to be certain something always happens, include an explicit case _: wildcard as a final fallback.
Performance note
For a small number of simple value comparisons, match and an equivalent if/elif chain perform comparably — there's no meaningful speed advantage either way. match's real benefit is readability once your logic starts checking structure rather than plain values — at that point, expressing the same logic with if/elif and manual type checks or key lookups tends to get noticeably messier and harder to follow.
When to reach for it
match is the right tool when you're parsing structured data (JSON-like dictionaries, API responses), dispatching behavior based on message or event type, or implementing something like a state machine with several distinct shapes of input to handle. For simple value equality checks — "is this string one of a few specific options" — a plain if/elif chain, or even a dictionary lookup, is often just as clear and doesn't require Python 3.10 or later.