match/case, introduced in Python 3.10 via PEP 634, looks like a switch statement at first glance but is fundamentally more powerful: it matches on the structure of a value ā its type, its contents, its attributes ā not just equality. This lesson teaches you to use it idiomatically instead of as a worse if/elif chain.
1Beyond switch: What "Structural" Actually Means
A traditional switch statement in languages like C or Java compares a single scalar value for equality against a list of constants. Python's match/case can do that ā case 200: behaves exactly like you'd expect ā but that's the simplest, least interesting case it supports. The 'structural' in structural pattern matching means each case describes a *shape*: a sequence of a certain length, a mapping with certain keys, an instance of a certain class with certain attribute values ā and the match succeeds only if the subject's actual structure fits that shape.
Critically, match/case doesn't just test structure ā it simultaneously *destructures* and *binds* matching parts to new names in one motion. case (x, 0): both checks 'is this a 2-tuple whose second element is 0' and binds x to the first element, available for use in the case body. Achieving the same thing with if/elif requires separate length and equality checks followed by manual indexing ā more code, more chances for an off-by-one mistake.
This is precisely why pattern matching pairs so naturally with the discriminated unions from the previous lesson: case Success(data=data): checks 'is this an instance of Success' and extracts its data attribute in the same breath, replacing an isinstance() check followed by a manual .data access with one declarative line.
def http_status_text(code: int) -> str:
match code:
case 200:
return "OK"
case 404:
return "Not Found"
case _:
return "Unknown"'On the x-axis at 3'
2Class Patterns: Matching Type and Attributes Together
case Success(data=data): is a class pattern. It performs an implicit isinstance(result, Success) check first ā if that fails, the case doesn't match at all ā and only then destructures the named attribute data into a locally bound variable of the same name. You can bind to a different local name too, with case Success(data=payload):, and you can match multiple attributes at once: case Point(x=0, y=y): matches only points on the y-axis, binding y.
For this to work on your own classes, Python needs to know which positional attributes are available for matching without keywords ā that's controlled by a class attribute called __match_args__, which dataclasses (the next lesson) sets automatically based on field order. That's why case Point(0, y): (positional) works out of the box for a dataclass Point with fields x, y, without you writing any extra matching logic.
This turns match/case over a discriminated union into something close to exhaustive, type-safe branching: each case both identifies which variant you're handling and gives you direct, already-typed access to that variant's specific fields, with a final case _: to handle anything unaccounted for.
def describe(point: tuple) -> str:
match point:
case (0, 0):
return "Origin"
case (x, 0):
return f"On the x-axis at {x}"
case (x, y):
return f"Point at ({x}, {y})"isinstance check + attribute destructure, together
3Guards, Wildcards, and Writing Exhaustive Matches
A guard (case ["move", direction] if direction in (...):) lets you attach an arbitrary boolean condition to a case, evaluated only after the structural pattern already matched. This is the escape hatch for logic that can't be expressed structurally ā value ranges, membership checks, cross-field comparisons ā while keeping the shape-matching part of the case declarative and readable.
The wildcard _ in case _: matches anything and binds nothing; it's the idiomatic way to write a catch-all final case, equivalent to else in an if/elif chain. Unlike a bare variable name, _ is guaranteed by the language to never be treated as a capture pattern that shadows an outer variable ā it's specifically the 'match anything, ignore it' pattern.
Because match doesn't require you to write an exhaustive set of cases (an unmatched value simply falls through with no error, unless you add case _: that raises), disciplined use means always ending non-trivial matches with an explicit case _: that either handles the default sensibly or raises a clear error ā silently doing nothing on an unmatched pattern is a common source of confusing, hard-to-trace bugs.
match result:
case Success(data=data):
print(f"Got: {data}")
case Failure(error=msg):
print(f"Failed: {msg}")Explicit catch-all ā never leave a match silently incomplete
4Step-by-Step Breakdown
match/case isn't Python's switch statement. It's a structural pattern matcher that can destructure, type-check, and bind variables all in one clause ā let's see why that matters.
At its simplest, match/case compares a value against literal patterns, similar to a switch statement in other languages.
The real power is structural matching: you can destructure sequences and bind their elements directly in the case clause.
Checkpoint: In case (x, 0):, what does x become inside that branch?
- āIt is bound to whatever value is in the first position of the tuple
- āIt must literally match a variable named x elsewhere in scope
Class patterns match by type AND destructure attributes at once ā perfect for the discriminated unions from the last lesson.
Guards add extra conditions with 'if' inside a case ā the pattern must match AND the guard must be true.
Checkpoint: What does the if clause in case ["move", direction] if direction in (...) do?
- āIt adds a guard condition ā the case only matches if the pattern matches AND the condition is true
- āIt always runs regardless of whether the pattern matched
Pattern matching pairs beautifully with discriminated unions and dataclasses ā next, we formalize those with the dataclasses module itself.
Guard a Real Match Case. Finish http_status_text(): a guard clause applies extra logic beyond literal matching.
Level Up š
Advanced cheat sheets, SEO tricks, and interview prep for this topic.
Browser Support
Fully supported (via server-side Python execution).
Fully supported (via server-side Python execution).
Fully supported (via server-side Python execution).
Fully supported (via server-side Python execution).
Best Practices
Always include an explicit case _: for non-trivial matches
match doesn't require exhaustiveness and silently does nothing if no case matches. An explicit wildcard case ā even one that just raises ValueError(f'Unhandled case: {value}') ā turns a silent bug into a loud, debuggable one.
Reach for match/case specifically when you need structure, not just equality
For a simple value-to-value lookup, a dict or a plain if/elif is often more readable than match/case. Pattern matching earns its complexity when you're destructuring sequences, mappings, or class instances.
Frequent Bugs
Writing case SomeClass: (uppercase, no parentheses) intending a type check, and being confused when it silently captures the value as a variable named SomeClass instead.
Bare names in a pattern are always capture patterns, even if they happen to match a class name in scope. For a type check, use case SomeClass():, with parentheses, which is a class pattern.
Real-World Examples
Routing a Simple Command-Line Interpreter
A REPL-style tool needs to parse space-separated user commands like "move up" or "quit" into distinct actions, with graceful handling of malformed input.
def handle(command: str) -> str:
match command.split():
case ["quit"]:
return "Goodbye!"
case ["move", direction] if direction in ("up", "down", "left", "right"):
return f"Moving {direction}"
case ["move", *rest]:
return f"Invalid move arguments: {rest}"
case _:
return f"Unknown command: {command}"