W3docs

Python match Statement

Learn Python structural pattern matching with match/case: literals, sequences, mappings, class patterns, guards, and the wildcard — with examples.

Python 3.10 introduced structural pattern matching via the match statement — a powerful way to branch on the shape and content of data, not just on equality. This chapter covers everything from basic match/case syntax to advanced patterns like sequence unpacking, mapping patterns, class patterns, guards, and real-world use cases.

Before reading this chapter you should be comfortable with Python if/else, Python functions, and basic data structures (lists, tuples, dictionaries).

What Is Structural Pattern Matching?

Structural pattern matching lets you inspect an object's structure — its type, the values of its fields, the shape of a sequence — and execute different code depending on which pattern fits. It goes far beyond a simple if x == y check.

Consider routing an HTTP status code. With if/elif chains you write:

if status == 200:
    print("OK")
elif status == 404:
    print("Not Found")
elif status == 500:
    print("Internal Server Error")
else:
    print("Unknown status")

With match the intent is cleaner:

match status:
    case 200:
        print("OK")
    case 404:
        print("Not Found")
    case 500:
        print("Internal Server Error")
    case _:
        print("Unknown status")

The real advantage appears when the subject is a complex object — a tuple, a dictionary, or a dataclass — and you want to destructure it while matching.

Basic Syntax

match subject:
    case pattern1:
        # runs if subject matches pattern1
    case pattern2:
        # runs if subject matches pattern2
    case _:
        # wildcard — runs if nothing else matched

Rules to remember:

  • match and case are soft keywords — they are only keywords in this context and can still be used as variable names elsewhere in your code.
  • Each case block is tried in order; the first match wins and the others are skipped.
  • The case _: block is the wildcard — it always matches and acts as a catch-all default.
  • Python 3.10+ is required. Running this on Python 3.9 or earlier raises a SyntaxError.

Literal Patterns

The simplest pattern matches a concrete value: a number, a string, True, False, or None.

def http_status(status):
    match status:
        case 200:
            return "OK"
        case 404:
            return "Not Found"
        case 500:
            return "Internal Server Error"
        case _:
            return "Unknown status"

print(http_status(200))   # OK
print(http_status(404))   # Not Found
print(http_status(999))   # Unknown status

OR Patterns (|)

Use | inside a case to match any one of several literals:

def is_vowel(letter):
    match letter.lower():
        case "a" | "e" | "i" | "o" | "u":
            return True
        case _:
            return False

print(is_vowel("a"))   # True
print(is_vowel("b"))   # False
print(is_vowel("E"))   # True

OR patterns also work with numbers, None, and other literal types.

Capture Patterns

A capture pattern is a bare name (not a string literal, not a dotted name) that matches anything and binds the matched value to that name for use in the body:

def greet(name):
    match name:
        case "Alice":
            return "Hello, Alice!"
        case other:          # captures whatever was passed
            return f"Hello, {other}!"

print(greet("Alice"))    # Hello, Alice!
print(greet("Bob"))      # Hello, Bob!

other above is a capture pattern — it binds the matched value to the local variable other. This looks a lot like the wildcard _, but _ discards the value while a named capture keeps it.

Warning

A bare name in a case is always a capture, never a comparison. If you want to compare against a constant defined elsewhere, use a dotted name like Status.OK or wrap it in a guard (case x if x == my_constant:).

Sequence Patterns

A sequence pattern matches lists, tuples, or any sequence, and can destructure the elements into variables at the same time.

def process_point(point):
    match point:
        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):
            return f"Point at ({x}, {y})"

print(process_point((0, 0)))   # Origin
print(process_point((5, 0)))   # On x-axis at 5
print(process_point((0, 3)))   # On y-axis at 3
print(process_point((2, 4)))   # Point at (2, 4)

Using * to Capture the Rest

A *name inside a sequence pattern collects remaining elements, just like iterable unpacking:

def describe_list(items):
    match items:
        case []:
            return "empty list"
        case [single]:
            return f"one item: {single}"
        case [first, *rest]:
            return f"starts with {first!r}, then {len(rest)} more item(s)"

print(describe_list([]))              # empty list
print(describe_list([42]))            # one item: 42
print(describe_list([1, 2, 3, 4]))   # starts with 1, then 3 more item(s)

Use [first, *_] if you want to capture only the first element and discard the rest.

Mapping Patterns

A mapping pattern matches dictionaries (or any Mapping). You only specify the keys you care about — extra keys in the subject are ignored.

def process_event(event):
    match event:
        case {"type": "click", "button": button}:
            return f"Mouse click: button {button}"
        case {"type": "keypress", "key": key}:
            return f"Key pressed: {key!r}"
        case {"type": action}:
            return f"Other event: {action}"
        case _:
            return "Unknown event"

print(process_event({"type": "click", "button": 1}))
# Mouse click: button 1
print(process_event({"type": "keypress", "key": "Enter"}))
# Key pressed: 'Enter'
print(process_event({"type": "resize", "width": 800}))
# Other event: resize
print(process_event({}))
# Unknown event

Key point: a mapping pattern never fails because of extra keys in the subject. {"type": "click", "button": button} matches even if the event also contains "x" and "y" coordinates.

To capture the remaining key/value pairs, use **rest:

match event:
    case {"type": "click", **rest}:
        print(f"Click event with extra data: {rest}")

Class Patterns

A class pattern matches an instance of a specific class and extracts its attributes. This is especially useful with dataclasses because their attributes are exposed by name automatically.

from dataclasses import dataclass

@dataclass
class Point:
    x: float
    y: float

@dataclass
class Circle:
    center: Point
    radius: float

def describe_shape(shape):
    match shape:
        case Point(x=0, y=0):
            return "Point at origin"
        case Point(x=x, y=y):
            return f"Point at ({x}, {y})"
        case Circle(center=Point(x=cx, y=cy), radius=r):
            return f"Circle centered at ({cx}, {cy}) with radius {r}"
        case _:
            return "Unknown shape"

print(describe_shape(Point(0, 0)))           # Point at origin
print(describe_shape(Point(3, 4)))           # Point at (3, 4)
print(describe_shape(Circle(Point(1, 2), 5)))# Circle centered at (1, 2) with radius 5

Notice the nested class pattern in the Circle case: Point(x=cx, y=cy) is matched inside the Circle pattern. Patterns can be composed arbitrarily deep.

For built-in types like int, str, float, and bool you can use positional patterns with a single argument:

def handle_input(value):
    match value:
        case (int() | float()) as number:
            return f"Got a number: {number}"
        case str() as text:
            return f"Got text: {text!r}"
        case _:
            return "Unknown type"

print(handle_input(3.14))    # Got a number: 3.14
print(handle_input("hello")) # Got text: 'hello'
print(handle_input([1, 2]))  # Unknown type

The as keyword (the AS pattern) binds the whole matched value to a name, even after a type check.

Guards

A guard is an if condition added after a pattern. The case only matches when the pattern fits and the guard evaluates to True.

def classify_number(n):
    match n:
        case 0:
            return "zero"
        case x if x < 0:
            return f"{x} is negative"
        case x if x % 2 == 0:
            return f"{x} is positive and even"
        case x:
            return f"{x} is positive and odd"

print(classify_number(0))    # zero
print(classify_number(-5))   # -5 is negative
print(classify_number(4))    # 4 is positive and even
print(classify_number(7))    # 7 is positive and odd

Guards are evaluated after the structural pattern matches, so captured variables are available inside them. A failed guard does not prevent later cases from being tried.

The Wildcard Pattern _

_ is the universal catch-all. It matches any value and binds nothing (the value is discarded). It should be the last case in a match block. Without it, a match that finds no matching case simply does nothing — no error is raised.

def describe(value):
    match value:
        case 0:
            return "zero"
        case _:
            return f"something else: {value!r}"

print(describe(0))     # zero
print(describe(99))    # something else: 99
print(describe("hi"))  # something else: 'hi'

_ can also appear inside a pattern to ignore specific parts:

match point:
    case (_, 0):
        print("On the x-axis (x value doesn't matter)")
    case (0, _):
        print("On the y-axis (y value doesn't matter)")

Combining Patterns: A Real-World Example

The patterns above compose. Here is a text-adventure command parser that combines sequence patterns, guards, and the wildcard:

def run_command(command):
    match command.split():
        case ["quit"]:
            return "Quitting"
        case ["go", direction] if direction in ("north", "south", "east", "west"):
            return f"Going {direction}"
        case ["go", direction]:
            return f"Cannot go {direction!r} — try north, south, east, or west"
        case ["get", item]:
            return f"Picking up {item}"
        case ["drop", item]:
            return f"Dropping {item}"
        case ["inventory"]:
            return "Checking inventory"
        case [verb, *args]:
            return f"Unknown command {verb!r} with args {args}"
        case []:
            return "No command entered"

print(run_command("go north"))    # Going north
print(run_command("go up"))       # Cannot go 'up' — try north, south, east, or west
print(run_command("get sword"))   # Picking up sword
print(run_command("drop torch"))  # Dropping torch
print(run_command("quit"))        # Quitting
print(run_command(""))            # No command entered

Reading this top to bottom you can immediately understand every supported command — something that would require many more lines of if/elif logic to achieve the same clarity.

match vs. if/elif — When to Use Each

ScenarioBest choice
Simple equality against a few constantsEither; match is slightly cleaner
Matching on data structure / shapematch — far cleaner
Destructuring values while matchingmatch — cannot be done with if
Logic involving calculated conditions onlyif/elif
Python 3.9 or earlierif/elif (no match available)
Expressing a decision table clearlymatch

match is not a replacement for every if chain. When all branches check computed boolean conditions (e.g., if x > 10 and y < 5) an if/elif chain is natural. match shines when the condition is about the form of data.

Common Gotchas

Constant names are not matched by value

A bare name in a case is always a capture, never a lookup:

STATUS_OK = 200

match response_code:
    case STATUS_OK:          # WRONG — this captures into STATUS_OK, not compares!
        print("Success")

To compare against a named constant, use a dotted name (http.HTTPStatus.OK) or a guard:

match response_code:
    case x if x == STATUS_OK:
        print("Success")

match is not exhaustive by default

Unlike switch in some other languages, a match that has no matching case silently does nothing. Add a case _: if you need a guaranteed handler.

match requires Python 3.10+

Running a match block on Python 3.9 or earlier raises SyntaxError: invalid syntax. Check your version with python3 --version. See the Python getting started guide if you need to set up a modern Python environment.

Patterns are not boolean expressions

You cannot write case x > 5: — that is a guard, not a pattern. The structural part (case x) must come first, followed by an optional if guard_expression.

Summary

Pattern typeSyntax exampleWhat it matches
Literalcase 42:Exact value
ORcase "yes" | "y":Any of the alternatives
Wildcardcase _:Anything (discards value)
Capturecase x:Anything, binds to x
Sequencecase [a, b, *rest]:A sequence with at least 2 elements
Mappingcase {"key": val}:A dict containing the given keys
Classcase Point(x=0, y=y):An instance with matching attributes
AScase int() as n:Matches and binds whole value
Guardcase x if x > 0:Pattern + extra boolean condition

Practice

Practice
Which Python version first introduced the match statement?
Which Python version first introduced the match statement?
Practice
In a match block, what does a bare variable name in a case clause do?
In a match block, what does a bare variable name in a case clause do?
Practice
Which pattern type would you use to match a dict that contains at least a 'type' key and extract its value?
Which pattern type would you use to match a dict that contains at least a 'type' key and extract its value?

Now that you know how to branch on data structure, explore Python for loops to iterate over sequences, or Python enums to define the kind of typed constants that work well with class patterns.

Was this page helpful?