Python match-case: Structural Pattern Matching Guide

Quick answer: Python match-case performs structural pattern matching, not only literal switch-style comparison. It can match values, sequences, mappings, classes, OR patterns, and guarded cases, choosing the first successful case.

Python match-case infographic comparing literal, OR, sequence, mapping, and guarded patterns with fallback behavior
match-case selects the first matching pattern, so put specific cases before broad fallbacks and keep guards explicit.

match and case add structural pattern matching to Python 3.10 and later. They can replace long if/elif chains when code needs to branch by value, shape, or object structure.

The official Python match statement documentation explains the syntax, and PEP 634 defines the pattern matching behavior.

A match statement tests a subject against cases from top to bottom. The first case that matches runs. Use case _: as the default branch when no earlier case should handle the input.

Unlike a basic switch statement in some languages, Python pattern matching can inspect structure. A case can check whether the input is a list of a certain length, a dictionary with particular keys, or an object with selected attributes.

Match Literal Values

The simplest pattern matches exact values such as strings, numbers, booleans, or None.

command = "start"

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

This behaves like a readable switch-style branch. The underscore pattern catches anything that was not matched earlier.

Order matters. Put more specific cases before broad default cases so they have a chance to run.

Literal matches are a good fit for command names, status strings, modes, and small enumerations. They keep the branch table compact without hiding the values being handled.

Match Several Values In One Case

Use the | operator to match one of several literal patterns in a single branch.

status = "q"

match status:
    case "q" | "quit" | "exit":
        print("Leaving")
    case "h" | "help":
        print("Showing help")
    case _:
        print("Continuing")

This keeps related choices together. It is useful for command aliases, short options, and small sets of equivalent values.

Keep each branch focused. If the list of accepted values grows too large, a set lookup may be easier to maintain.

The OR pattern should mean that the alternatives really share the same behavior. If two choices start to need different validation or output, split them into separate cases. match/case destructures Python values, whereas Python fnmatch Library Pattern Guide applies shell-style wildcard patterns to names and paths.

Python Pool infographic showing a match statement, case patterns, subject value, and structural matching
Pattern matching: A match statement, case patterns, subject value, and structural matching.

Use Guards For Extra Conditions

A guard adds an if condition to a case. The pattern must match first, and then the guard must be true.

value = 42

match value:
    case int() if value > 0:
        print("positive integer")
    case int() if value < 0:
        print("negative integer")
    case 0:
        print("zero")
    case _:
        print("not an integer")

Guards are good for numeric ranges, extra validation, and checks that are clearer as conditions than as patterns.

Use guards sparingly. If every case needs a long guard, a normal if/elif chain may be simpler.

Guards also make captured names useful. The pattern can bind a value, and the guard can decide whether that bound value is acceptable for the branch.

Match Sequence Patterns

Sequence patterns match lists or tuples by their shape. They can also capture parts of the input.

tokens = ["move", 10, 20]

match tokens:
    case ["move", x, y]:
        print(f"Move to {x}, {y}")
    case ["quit"]:
        print("Quit")
    case [name, *args]:
        print(name, args)
    case _:
        print("Invalid command")

The first branch matches a three-item command. The *args pattern captures the remaining items in a longer command.

This style works well for tiny command languages, parsed text, and structured list data where the first item describes the action.

Sequence patterns should match the shapes your program actually accepts. Add a default branch for malformed input so unexpected shapes fail clearly.

Python Pool infographic mapping a subject through literal, sequence, mapping, class, and guard patterns
Case guards: A subject through literal, sequence, mapping, class, and guard patterns.

Match Mapping Patterns

Dictionary-like data can be matched by required keys. Captured names receive the matching values.

event = {"type": "click", "x": 12, "y": 8}

match event:
    case {"type": "click", "x": x, "y": y}:
        print(f"Click at {x}, {y}")
    case {"type": "keypress", "key": key}:
        print(f"Key pressed: {key}")
    case _:
        print("Unhandled event")

The click case requires all three keys. Extra keys can still be present; the pattern only requires the keys it mentions.

Mapping patterns are useful for JSON-like data, events, API payloads, and dictionaries returned by parsers.

They are also safer than indexing keys manually in every branch. A branch only runs when the required keys exist, so missing-key cases can fall through to a default handler.

Match Class Patterns

Class patterns match object types and can inspect attributes. Dataclasses are a clean way to demonstrate this style.

from dataclasses import dataclass

@dataclass
class Point:
    x: int
    y: int

point = Point(0, 5)

match point:
    case Point(x=0, y=y):
        print(f"on y axis at {y}")
    case Point(x=x, y=0):
        print(f"on x axis at {x}")
    case Point(x=x, y=y):
        print(f"point {x}, {y}")

The first matching class pattern runs. The example checks special axis cases before the broader point case.

Class patterns are powerful, but they should still be obvious. If a pattern becomes hard to read, move some logic into a well-named method or guard.

This style is most effective for simple data objects where attributes describe the state directly. For objects with complex behavior, method calls may communicate intent better than a large pattern.

Python Pool infographic comparing wildcard, capture, as-pattern, OR-pattern, and bound values
Capture values: Wildcard, capture, as-pattern, OR-pattern, and bound values.

Common Match Case Mistakes

Do not use match/case in code that must run on Python versions older than 3.10. Older interpreters will raise a syntax error before the program starts.

Do not put case _: before specific cases. It matches anything, so later cases would never run.

Do not assume a captured name compares against an existing name. Bare names in patterns capture values. Use literals, dotted constants, or guards when you need a comparison.

The practical rule is to use match when the structure of the input matters. Use ordinary conditionals for simple true/false checks, especially when there are only one or two branches.

Basic Literal And OR Patterns

A match statement evaluates its subject once and tries cases from top to bottom. Literal patterns are useful for commands and status values. An OR pattern combines alternatives that should share the same body. Put specific cases before broad patterns.

def label_status(status):
    match status:
        case 200 | 201:
            return "success"
        case 404:
            return "missing"
        case _:
            return "other"

print(label_status(201))
Python Pool infographic testing ordering, unreachable cases, exhaustiveness, and default behavior
Match checks: Ordering, unreachable cases, exhaustiveness, and default behavior.

Sequence And Mapping Patterns

Patterns can describe the shape of a list or tuple and bind names from it. Mapping patterns match required keys without requiring every key to be listed. This is useful for command payloads, parsed records, and small configuration objects where structure matters more than one exact value.

def describe(event):
    match event:
        case ["error", message]:
            return f"error: {message}"
        case {"kind": kind, "value": value}:
            return f"{kind}={value}"
        case _:
            return "unknown"

print(describe(["error", "timeout"]))

Guards And Readable Fallbacks

A guard adds an if condition after a pattern. Use it for a value constraint that is separate from the structural shape. Keep guards short, use a final wildcard fallback when unknown input is expected, and remember that a case with a wildcard does not bind a variable named underscore.

Frequently Asked Questions

Which Python version introduced match-case?

Structural pattern matching was introduced in Python 3.10.

Is match-case the same as a switch statement?

It can handle switch-like literal dispatch, but it also matches sequences, mappings, class patterns, OR patterns, and guards.

What does the underscore mean in match-case?

A standalone underscore is a wildcard pattern that matches any value and is commonly used as the final fallback.

What is a guard in match-case?

A guard is an if condition attached to a case; the pattern must match and the guard must evaluate true for that case to run.

Subscribe
Notify of
guest
6 Comments
Oldest
Newest Most Voted
Ariel
Ariel
4 years ago

Try not to overwrite inbuilt variables like ‘quit’ it is common conversation to use an underscore to avoid this: ‘quit_’

Python Pool
Admin
4 years ago
Reply to  Ariel

Yes. I’ve updated the post accordingly. Thank you for bringing it up!

Saeed Baig
Saeed Baig
4 years ago

“Also, with a match case, the code becomes more readable and manageable”
Well that depends on the usecase. Although I’m glad Python has match now, there are simple usecases where its overkill. Like that example for showing guards:

match n:
    case n if n < 0:
        print("Number is negative")
    case n if n == 0:
        print("Number is zero")
    case n if n > 0:
        print("Number is positive")

This is a good easy example to show how guards work, but obviously in real code you’d probably write this using a simpler if-chain:

if n < 0:
    print("Number is negative")
elif n == 0:
    print("Number is zero")
elif n > 0:
    print("Number is positive")

(and I don’t think this is ant slower cause you’d be performing the same #if-checks as the match-guard version).

TLDR: Don’t replace all your if-else chains with match; match is best for replacing simple if x == a chains, and pattern matching is also helpful (guards maybe), but use your best judgement.

Last edited 4 years ago by Saeed Baig
Python Pool
Admin
4 years ago
Reply to  Saeed Baig

Totally agreed

azraelgnosis
azraelgnosis
4 years ago

Hi, you’ve an error in the “Match Case Python for Function Overloading” section.Python doesn’t have function overloading (well, there’s multipledispatch); the first definition of calc is just overwritten. And is_int isn’t (re-)assigned in the text of the integer example, but assuming that this happens, you get false positives for values where n^2 == n^3 (e.g. n=0, n=1)

Python 3.10.0 (tags/v3.10.0:b494f59, Oct  4 2021, 19:00:18) [MSC v.1929 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> def calc(n:int): return (n**2)
...
>>> def calc(n:float): return (n**3)
...
>>> n = 9.5
>>> is_int = isinstance(n, int)
>>> match is_int:
...     case True : print("Square is :",calc(n))
...     case False : print("Cube is:", calc(n))
... # Cube is: 857.375
...
Cube is: 857.375
>>> n = 9
>>> is_int = isinstance(n, int)
>>> match is_int:
...     case True : print("Square is :",calc(n))
...     case False : print("Cube is:", calc(n))
... # Square is : 729
...
Square is : 729
>>>
Pratik Kinage
Admin
4 years ago
Reply to  azraelgnosis

Correct. I’ve updated the post accordingly.