🐍 Python Course · Stage 1 · Lesson 7 of 89 · match / case
Course Overview →

🟩 Start with the Beginner tab. Read it fully before moving on.

← Control Flow & Loops Comprehensions →
thecodex.expert · The Codex Family of Knowledge
Python

Structural Pattern Matching (match/case)

The match statement (Python 3.10+) matches a value against patterns that describe the structure of data — far more powerful than a simple switch on a single value.

Python 3.13 docs.python.org Last verified:
Canonical Definition

Structural pattern matching, introduced by PEP 634 in Python 3.10, is a match statement that compares a subject value against a series of case patterns. Patterns can match literals, capture values, destructure sequences and mappings, and match class instances by attribute.

🟩 Beginner

Smarter than a switch statement

What you’ll learn: The match statement (Python 3.10+) — matching a value against literal patterns, the wildcard _ case, combining patterns with |, and capturing/destructuring values.

How to read this tab: Requires Python 3.10+. Start with literal matching, then move to destructuring — that is where match beats if/elif.

⏱ 40 min📄 2 sections🔶 Prerequisite: Control Flow & Loops
The idea

A match statement looks at a value and checks it against several patterns, running the code for the first one that fits. It is like a smarter if/elif chain that can also look inside the data — matching not just values but their shape.

match requires Python 3.10 or newer. On older versions it is a syntax error. match and case are "soft keywords" — they only act as keywords inside a match statement, so existing variables named match or case still work elsewhere.

match basics

The simplest form of match compares a value against literal patterns — close to a switch statement in other languages, but with one crucial extra: the wildcard _ case.

Pythonmatch_basic.py
def describe_status(code):
    match code:
        case 200:
            return "OK"
        case 404:
            return "Not Found"
        case 500:
            return "Server Error"
        case _:                  # wildcard — matches anything (the "default")
            return "Unknown"

print(describe_status(200))   # OK
print(describe_status(999))   # Unknown

# Combine patterns with | (OR)
def category(code):
    match code:
        case 200 | 201 | 204:
            return "Success"
        case 400 | 401 | 403 | 404:
            return "Client Error"
        case 500 | 502 | 503:
            return "Server Error"
        case _:
            return "Other"

print(category(404))   # Client Error
match needs Python 3.10 or newer

Structural pattern matching was added in Python 3.10 (October 2021). On older versions, match is a syntax error. Check with python --version. match and case are "soft keywords" — they are only keywords inside a match statement, so existing code using them as variable names still works.

The real power is destructuring — matching the shape of data. case ["go", direction] matches a two-element list starting with "go" and captures the second element. This is what makes match fundamentally more capable than a switch on a single value.

Capturing and destructuring

The real power of match appears when patterns capture values and destructure sequences — pulling apart the structure of the data in the pattern itself.

Pythondestructure.py
def handle_command(command):
    match command.split():
        case ["quit"]:
            return "Quitting"
        case ["go", direction]:           # capture the second word
            return f"Moving {direction}"
        case ["pick", "up", *items]:      # capture the rest as a list
            return f"Picking up: {items}"
        case _:
            return "Unknown command"

print(handle_command("go north"))         # Moving north
print(handle_command("pick up sword shield"))  # Picking up: ['sword', 'shield']

# Destructuring a coordinate tuple
def quadrant(point):
    match point:
        case (0, 0):
            return "origin"
        case (x, 0):                      # captures x, requires y == 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(quadrant((0, 0)))   # origin
print(quadrant((5, 0)))   # on x-axis at 5
print(quadrant((3, 4)))   # point at (3, 4)

✅ Beginner tab complete

  • I can write a match with literal cases and a _ wildcard default
  • I can combine patterns with | (OR)
  • I can destructure a sequence: case [x, y] or case ["go", direction]
  • I know a bare name in a case captures, it does not compare

Continue to Comprehensions →

🔵 Intermediate

Class patterns, guards, and mappings

What you’ll learn: Matching class instances by attribute (Circle(radius=r)), adding if guards to patterns, matching dict structure (mapping patterns), and the as keyword for capture.

How to read this tab: Class patterns plus dataclasses are a powerful combination — they replace long isinstance chains with clean declarative code.

⏱ 35 min📄 2 sections🔶 Prerequisite: Beginner tab
💡

Class patterns + dataclasses replace isinstance chains. Instead of if isinstance(s, Circle): ... elif isinstance(s, Rectangle): ..., write case Circle(radius=r): and case Rectangle(width=w, height=h): — matching the type and extracting attributes together.

Class patterns

Patterns can match instances of a class and simultaneously extract their attributes — exceptionally useful for processing structured objects without a chain of isinstance checks.

Pythonclass_patterns.py
from dataclasses import dataclass

@dataclass
class Circle:
    radius: float

@dataclass
class Rectangle:
    width: float
    height: float

@dataclass
class Triangle:
    base: float
    height: float

def area(shape):
    match shape:
        case Circle(radius=r):
            return 3.14159 * r * r
        case Rectangle(width=w, height=h):
            return w * h
        case Triangle(base=b, height=h):
            return 0.5 * b * h
        case _:
            raise ValueError("Unknown shape")

print(area(Circle(5)))            # 78.53...
print(area(Rectangle(4, 6)))      # 24
print(area(Triangle(3, 8)))       # 12.0

# Positional class patterns using __match_args__
@dataclass
class Point:
    x: int
    y: int
    # dataclasses set __match_args__ = ('x', 'y') automatically

def origin_check(p):
    match p:
        case Point(0, 0):       # positional — uses __match_args__
            return "at origin"
        case Point(x, y):
            return f"at ({x}, {y})"

print(origin_check(Point(0, 0)))  # at origin

A bare name captures — it never compares. case x: matches anything and binds it to x. To compare against a constant, use a dotted name (case Color.RED:) or a literal. case RED: creates a new variable RED rather than comparing to an existing one — a frequent and silent mistake.

Guards and mapping patterns

Pythonguards_mapping.py
# Guards — an if condition attached to a pattern
def classify(point):
    match point:
        case (x, y) if x == y:           # guard
            return "on diagonal"
        case (x, y) if x > 0 and y > 0:
            return "first quadrant"
        case (x, y):
            return "elsewhere"

print(classify((3, 3)))   # on diagonal
print(classify((2, 5)))   # first quadrant

# Mapping patterns — match dict structure (e.g. JSON)
def handle_event(event):
    match event:
        case {"type": "click", "x": x, "y": y}:
            return f"Click at ({x}, {y})"
        case {"type": "key", "key": key}:
            return f"Key pressed: {key}"
        case {"type": event_type}:
            return f"Unknown event: {event_type}"
        case _:
            return "Malformed event"

print(handle_event({"type": "click", "x": 10, "y": 20}))  # Click at (10, 20)
print(handle_event({"type": "key", "key": "Enter"}))      # Key pressed: Enter

# Capture with 'as'
def describe(obj):
    match obj:
        case [Point(0, 0) as p]:
            return f"single origin point: {p}"
        case [*points]:
            return f"{len(points)} points"
Commonly confused
A bare name in a case captures — it does not compare. case x: matches anything and binds it to x. To compare against an existing constant, use a dotted name (case Color.RED:) or a literal. case RED: where RED is a variable will NOT compare to RED — it captures into a new variable named RED.
match is not faster than if/elif for simple value checks. Its advantage is readability and structural destructuring, not speed. For a simple value-equality switch, if/elif is perfectly fine. Reach for match when you are destructuring data or matching shapes.

✅ Intermediate tab complete

  • I can match a class instance and extract its attributes in one pattern
  • I can add a guard: case (x, y) if x == y
  • I can match dict structure with mapping patterns
  • I know mapping patterns match a SUBSET of keys (extra keys are ignored)

Continue to Comprehensions →

🔴 Expert

PEP 634 semantics and __match_args__

What you’ll learn: The formal matching semantics — irrefutable patterns, no fall-through, __match_args__ for positional class patterns, and why str/bytes are excluded from sequence patterns.

How to read this tab: Read when implementing complex pattern matching or designing classes meant to be matched positionally.

⏱ 20 min📄 1 section🔶 Prerequisite: After Stage 2

Match semantics, irrefutability, and __match_args__

PEP 634 defines the formal semantics. Patterns are tried top to bottom; the first matching case runs and no others (no fall-through, unlike C's switch). A pattern can be irrefutable — guaranteed to match — such as a bare capture pattern (case x:) or the wildcard (case _:). An irrefutable pattern must be last, because any case after it is unreachable; the compiler does not enforce this but the pattern can never trigger.

Pythonmatch_internals.py
# __match_args__ controls positional class patterns
class Point:
    __match_args__ = ("x", "y")   # positional order for matching
    def __init__(self, x, y):
        self.x = x
        self.y = y

# Now Point(0, 0) in a pattern checks x==0, y==0 positionally
match Point(1, 2):
    case Point(1, y):     # uses __match_args__ to map position 0 -> x
        print(f"x is 1, y is {y}")   # x is 1, y is 2

# Sequence patterns work on any Sequence EXCEPT str/bytes/bytearray
# (deliberately — so "abc" is not treated as ['a','b','c'])
match [1, 2, 3]:
    case [first, *rest]:
        print(first, rest)   # 1 [2, 3]

# The compiler emits MATCH_SEQUENCE, MATCH_MAPPING, MATCH_CLASS opcodes
import dis
def f(x):
    match x:
        case [a, b]: return a + b
        case _: return 0
dis.dis(f)   # shows MATCH_SEQUENCE, GET_LEN, etc.

Sequence patterns deliberately exclude str, bytes, and bytearray — matching "abc" against case [a, b, c] fails rather than destructuring into characters, because treating strings as character sequences in pattern matching is almost always a bug. Mapping patterns match a subset of keys: case {"type": t} matches any dict containing a "type" key, ignoring other keys — unlike sequence patterns which match the full length unless a star pattern is used.

✅ Expert tab complete

  • I know patterns are tried top-to-bottom with no fall-through
  • I know __match_args__ defines the positional order for class patterns
  • I know str/bytes are deliberately excluded from sequence patterns

Continue to Comprehensions →

Sources

1
Python Language Reference §8.6 — The match statement. docs.python.org/3/reference/compound_stmts.html#the-match-statement.
2
PEP 634 — Structural Pattern Matching: Specification. peps.python.org/pep-0634/.
3
PEP 636 — Structural Pattern Matching: Tutorial. peps.python.org/pep-0636/.
4
PEP 635 — Structural Pattern Matching: Motivation and Rationale. peps.python.org/pep-0635/.
Source confidence: High Last verified: Primary source: Python Language Reference §8.6 (PEP 634)