CHAPTER 10 · BEGINNER4 min read

Pattern matching with match and case

Express branching rules by the shape of data, add guards for conditions, and keep captures readable.

Python 3.12+Free to readModule 3 of 13
YOUR NEXT SMALL WINS

By the end, you’ll be able to…

  • Distinguish a structural pattern from an equality comparison.
  • Match sequences, mappings, and class instances with guards.
  • Use wildcards and ordered cases without hiding simple logic.

01A branch can describe a shape

An if/elif chain asks a sequence of questions. A match statement compares one value against one or more patterns and runs the first case that fits. Patterns can do more than compare equality: a sequence pattern can unpack positions, a mapping pattern can require keys, and a class pattern can read attributes. The shape has to match before the branch is selected.

Read this command parser from top to bottom. The first case only matches a two-item list whose first item is the string 'move'. The names x and y are captures: they receive the remaining values. The wildcard case is a catch-all. Once a case matches, its block runs and the other cases are skipped; there is no implicit fall-through.

commands.pyPython
def describe(command):    match command:        case ["move", x, y]:            return f"move to ({x}, {y})"        case ["say", message] if message:            return f"say: {message}"        case {"kind": "quit"}:            return "quit"        case _:            return "unknown command"print(describe(["move", 2, 5]))print(describe(["say", "hello"]))print(describe({"kind": "quit"}))print(describe(["say", ""]))
Outputstdout
move to (2, 5)
say: hello
quit
unknown command

02Guards add a condition

A guard is an if expression written after a pattern. The pattern must match first, then the guard is evaluated with any names captured by that pattern. If the guard is false, matching continues with the next case. Guards keep shape checks and range checks together without turning every branch into a nested if statement.

Notice that the first numeric case is a literal and the later cases capture a value before testing it. A bare name in a pattern is a capture, not a comparison with an existing variable. That rule is one of the most common sources of confusion in pattern matching. Use literals, dotted names, or enum members when you mean a fixed value.

scores.pyPython
def classify(score):    match score:        case 0:            return "zero"        case n if n < 0:            return "negative"        case n if n < 60:            return "review"        case n if n <= 100:            return "pass"        case _:            return "out of range"for value in (0, -3, 59, 60, 101):    print(value, classify(value))
Outputstdout
0 zero
-3 negative
59 review
60 pass
101 out of range

03Objects and mappings have shapes too

Dataclasses and ordinary classes can participate in class patterns. A dataclass gets a useful __match_args__ automatically, so Point(0, 0) can match its first two attributes positionally. You can also use keyword patterns such as Point(x=0, y=y), which makes the intent clearer when a class has several fields.

Mapping patterns are useful at data boundaries. They require the listed keys to exist but allow extra keys, which makes them a good fit for event payloads that may grow over time. Sequence patterns are stricter about the number of positions unless you use a starred capture. Choose the pattern that describes the contract you actually require.

events.pyPython
from dataclasses import dataclass@dataclassclass Point:    x: int    y: intdef quadrant(point):    match point:        case Point(0, 0):            return "origin"        case Point(0, y):            return f"y-axis at {y}"        case Point(x, 0):            return f"x-axis at {x}"        case Point(x, y) if x > 0 and y > 0:            return "quadrant I"        case Point():            return "another quadrant"print(quadrant(Point(0, 0)))print(quadrant(Point(0, 7)))print(quadrant(Point(4, 0)))print(quadrant(Point(4, 3)))print(quadrant(Point(-2, 5)))
Outputstdout
origin
y-axis at 7
x-axis at 4
quadrant I
another quadrant

04Keep patterns small and exhaustive

Pattern matching is a tool for data with recognizable shapes, not a replacement for every if statement. A simple equality check, a range comparison, or a short if/elif chain is often easier to read. Reach for match when you are unpacking a command, classifying an event, walking a tree of records, or handling several record shapes through one interface.

Order cases from most specific to most general. Put a wildcard or a broad capture last so it does not hide a case you meant to write. A wildcard is not required, but without one a value that matches no pattern falls through silently. Add a final case that raises an error or returns an explicit 'unsupported' result when the input space should be closed.

dispatch.pyPython
def handle(event):    match event:        case {"type": "ready", "count": count} if count > 0:            return f"ready with {count}"        case {"type": "error", "message": message}:            return f"error: {message}"        case _:            return "ignored"print(handle({"type": "ready", "count": 2}))print(handle({"type": "error", "message": "offline"}))print(handle({"type": "ready", "count": 0}))
Outputstdout
ready with 2
error: offline
ignored
LET IT CLICK

The ideas worth keeping.

  • A match statement selects the first pattern that fits, not every pattern that fits.
  • Guards and captures make data-shape rules explicit, but bare names always capture.
  • Keep a deliberate fallback case and prefer if/elif when the data has no useful structure to unpack.
TURN UNDERSTANDING INTO SOMETHING YOU CAN DO

Make it yours.

Type and test your code right here in the interactive workspace. Run your solution, check the output, and compare with the worked guide when ready.

01

Route a command

Write route(command) that matches a two-item ['GET', path] command, a three-item ['POST', path, body] command, and returns 'unsupported' for anything else. Print one example of each.

your_turn_1.py🐍Python
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.
Explore a worked solution
one_possible_solution.pyPython
def route(command):    match command:        case ["GET", path]:            return f"read {path}"        case ["POST", path, body]:            return f"write {path}: {body}"        case _:            return "unsupported"print(route(["GET", "/health"]))print(route(["POST", "/tasks", "ship it"]))print(route(["DELETE", "/tasks"]))

Each sequence pattern states the required length and position. The wildcard keeps an unexpected shape from failing silently with no result.

02

Guard an event

Write accept(event) that requires a dictionary with type 'reading', a numeric value, and a value greater than or equal to zero. Return a formatted message for accepted readings and 'rejected' otherwise.

your_turn_2.py🐍Python
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.
Explore a worked solution
one_possible_solution.pyPython
def accept(event):    match event:        case {"type": "reading", "value": value} if isinstance(value, (int, float)) and value >= 0:            return f"accepted {value}"        case _:            return "rejected"print(accept({"type": "reading", "value": 12}))print(accept({"type": "reading", "value": -1}))print(accept({"type": "other", "value": 4}))

The mapping pattern checks the required keys, while the guard checks the value's type and range. Both conditions must pass before the first case is selected.

A SMALL QUESTION. A USEFUL NEXT STEP.

What stayed with you?

Check the ideas while they’re fresh. Your answers stay in this page and feedback appears as you go.

A LITTLE ACTIVE RECALL0 / 3 answered
01What does a bare name such as case value: do?
02When a match case succeeds, what happens next?
03Which situation is usually clearer as an if/elif chain?
⚡CODING CHALLENGE
🔒Required to Mark Lesson Complete

Route a command

Write route(command) that matches a two-item ['GET', path] command, a three-item ['POST', path, body] command, and returns 'unsupported' for anything else. Print one example of each.

EXPECTED OUTPUT
route(["GET" "/health"])
route(["POST" /tasks "ship it"])
route(["DELETE" "/tasks"])
challenge.pyPython 3
Ctrl + ↵
Execution Console
Click "Run & Verify" (or press Ctrl + Enter) to test your code against the challenge.

Keep exploring.

The official references behind this chapter, when you want a second angle or the full details.

One chapter closer.

Reading is a start. Trying it yourself makes the connection.

Solve the hands-on coding challenge in the knowledge section to verify your code and unlock chapter completion.