Python Module 3 – Control flow: decisions, loops and pattern matching
Pattern matching with match and case
Compare a value with literal, OR and wildcard patterns in a Python match statement, capture values, add guards and avoid the bare-name capture trap.
What you will learn
- Match literals, OR patterns and the wildcard
- Capture values and add guards
- Explain why case NAME captures instead of comparing
Before you start
On this page
A match statement takes one value and checks it against a list of patterns, one case at a time, until one of
them fits. It can replace a long chain of if and elif tests that all look at the same value, and it can do more
than compare: a pattern can also pull values out into names for the code that follows.
This lesson covers the patterns you will use most: fixed values, alternatives, a catch-all, names that capture the value, and conditions called guards. The next lesson takes patterns further, into lists, dictionaries and objects.
How a match statement works
The value after match is the subject. Python evaluates it once and then tries the case lines from top to
bottom. The first case whose pattern fits, and whose guard (if it has one) is true, runs its indented block. Then the
whole statement is finished: no other case runs, even if it would have fitted too, and there is no break to write.
How a match statement chooses a case
Text description of the diagram
The diagram is a flow chart that runs from top to bottom.
- Python evaluates the subject, the expression after match, once.
- It takes the next case, starting with the first one.
- If that case's pattern does not fit the subject, it goes back to step 2 with the case after it.
- If the pattern fits, Python checks the guard, the if part after the pattern. A case without a guard counts as true here. If the guard is false, it goes back to step 2.
- If the guard is true, Python runs that case's block and skips every other case.
- When no case is left to try, the match statement ends without running any block and without raising an error.
If no case fits, the statement does nothing at all. Python raises no error, which is convenient and, as you will see, also an easy way to hide a bug.
Literal patterns, OR patterns and the wildcard
The simplest pattern is a literal: a number, a string, True, False or None written in the case line. It
fits when the subject equals it. This calculator chooses what to do from the operator the user typed:
def calculate(a, operator, b):
match operator:
case "+":
return a + b
case "-":
return a - b
case "*" | "x" | "×":
return a * b
case "/" | "÷":
return a / b
case _:
raise ValueError(f"unknown operator: {operator!r}")
print(calculate(7, "+", 5))
print(calculate(7, "×", 5))
print(calculate(7, "/", 2))
try:
print(calculate(7, "^", 2))
except ValueError as error:
print("ValueError:", error) Output
12 35 3.5 ValueError: unknown operator: '^'
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 calculator.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Three kinds of pattern are at work:
case "+":is a literal pattern. It fits whenoperator == "+".case "*" | "x" | "×":is an OR pattern: the|lists alternatives, and the case fits if any one of them does. People type multiplication in several ways, and one case handles them all.case _:is the wildcard. The underscore fits any subject, so a lastcase _handles everything the cases above it did not. Here it raises aValueErrorthat names the operator the calculator does not know.
True, False and None are the exception to “equals”: they are compared by identity (is), so case True: does
not fit the number 1, even though 1 == True.
A common mistake: no case for the rest
Leave out the wildcard and an unexpected subject simply falls through the whole statement:
def size_name(code):
match code:
case "S":
return "small"
case "M":
return "medium"
case "L":
return "large"
print(size_name("M"))
print(size_name("XL")) Output
medium None
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 sizes.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
"XL" fits none of the three cases, so the match does nothing, the function reaches its end without a return, and
it returns None. The program carries on with a wrong value and fails later, somewhere else, which makes the bug hard
to trace. Unless doing nothing really is right, end with case _: and return a clear value or raise an exception,
as the calculator does.
Capture patterns and guards
A bare name in a pattern, such as code, is a capture pattern. It fits any subject and assigns the subject to
that name, which the case can then use. On its own that matches everything, so it is usually combined with a
guard: if and a condition after the pattern. Python checks the guard only once the pattern has fitted; if the
guard is false, it goes on to the next case.
Every HTTP response carries a three-digit status code, and the HTTP standard sorts them into five classes by their first digit. This function names two common codes exactly and classifies the rest:
def describe(status):
match status:
case 200:
return "200 OK"
case 404:
return "404 Not Found"
case code if 100 <= code <= 199:
return f"{code}: informational"
case code if 200 <= code <= 299:
return f"{code}: successful"
case code if 300 <= code <= 399:
return f"{code}: redirection"
case code if 400 <= code <= 499:
return f"{code}: client error"
case code if 500 <= code <= 599:
return f"{code}: server error"
case _:
return f"{status}: not an HTTP status code"
for status in [200, 204, 301, 404, 429, 503, 99]:
print(describe(status)) Output
200 OK 204: successful 301: redirection 404 Not Found 429: client error 503: server error 99: not an HTTP status code
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 http_status.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Notice the order of the cases. 200 is also in the range of the “successful” guard, but case 200 comes first, so it
wins. Put specific cases above general ones: a broad case placed early would answer for every value it covers, and
the cases below it would never run.
The guards compare numbers, so describe("404"), with the code as text, would raise a TypeError inside the first
guard: a guard that fails with an error stops the whole match. The next lesson shows patterns that check the type
of the subject before any guard runs.
The capture trap: a bare name never compares
Because a bare name captures, it cannot be used to compare with a constant, and the mistake is easy to make because
the code reads naturally. The program below means “if the status is the value of SHIPPED”, but that is not what
it says:
SHIPPED = "shipped"
for status in ["packed", "shipped", "cancelled"]:
match status:
case "packed":
print(status, "-> being packed")
case SHIPPED:
print(status, "-> on its way")
print("SHIPPED is now", repr(SHIPPED)) Output
packed -> being packed shipped -> on its way cancelled -> on its way SHIPPED is now 'cancelled'
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 capture_trap.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
case SHIPPED: fits every subject and assigns it to SHIPPED. The cancelled order is reported as on its way, and
because this loop runs at the top level of the file, the constant itself has been overwritten with 'cancelled'.
Python catches one form of this mistake for you. When a capture pattern without a guard is followed by more cases, those cases could never run, and Python refuses to run the file:
SHIPPED = "shipped"
def label(status):
match status:
case SHIPPED:
return "on its way"
case _:
return "something else"
print(label("cancelled")) Output (exit status 1)
Printed as an error (standard error)
File "capture_error.py", line 6
case SHIPPED:
^^^^^^^
SyntaxError: name capture 'SHIPPED' makes remaining patterns unreachable
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 capture_error.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
To compare with a named value, use a dotted name, such as Status.SHIPPED. A name with a dot in it is a
value pattern: Python looks the value up and compares the subject with ==. An enumeration (an Enum class)
is a natural home for such names, and a StrEnum’s members are strings, so they compare equal to text such as
"shipped" read from a file or typed by a user:
from enum import StrEnum
class Status(StrEnum):
PACKED = "packed"
SHIPPED = "shipped"
def label(status):
match status:
case Status.PACKED:
return "being packed"
case Status.SHIPPED:
return "on its way"
case _:
return "unknown status"
for status in ["packed", "shipped", "cancelled"]:
print(status, "->", label(status)) Output
packed -> being packed shipped -> on its way cancelled -> unknown status
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 status_enum.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
The rule to remember: literals and dotted names compare; a bare name captures, and the single underscore matches
anything without capturing. If you would rather not write a class, a literal (case "shipped":) works too.
match and case are soft keywords
match and case are keywords only where a match statement can stand. Everywhere else they are ordinary names, so
programs written before Python 3.10 that use a variable called match still run. The keyword module calls them
soft keywords:
import keyword
import re
print("match is a keyword:", keyword.iskeyword("match"))
print("match is a soft keyword:", keyword.issoftkeyword("match"))
print("soft keywords:", keyword.softkwlist)
# Outside a match statement, match is an ordinary name.
match = re.search(r"\d+", "Invoice 381 is paid")
print(match.group()) Output
match is a keyword: False match is a soft keyword: True soft keywords: ['_', 'case', 'match', 'type'] 381
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 soft_keywords.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
The underscore is on the list because it is special inside patterns only, and type because it starts a type alias
statement; outside those places, all four are names you may use.
Version note
The match statement is new in Python 3.10 (PEP 634), so code that uses it needs Python 3.10 or newer; older versions
reject it with a SyntaxError. This track runs Python 3.14. StrEnum is newer still, from Python 3.11.
match or if and elif?
Both choose between blocks of code, and many choices can be written either way. Reach for match when several cases
look at the same subject: each case then reads as one line, “this value” or “one of these values”, and the patterns
of the next lesson can take the subject apart while they test it. Keep if and elif for two or three tests, and for
conditions that involve different values, such as if age < 18 and not signed_by_parent:. The two share one habit
worth keeping: when nothing fits, nothing runs, so end a match with case _ and an if chain with else whenever
every value needs an answer.
Key takeaways
matchevaluates the subject once and runs the first case whose pattern fits and whose guard is true; there is no fall-through and nobreak.- Literal patterns compare with
==(True,FalseandNonewithis),|gives alternatives, andcase _catches everything else. - When no case fits, nothing happens and no error is raised, so end with
case _unless doing nothing is right. - A bare name captures the subject; a guard (
case code if code < 200:) adds a condition that is checked after the pattern fits. - To compare with a constant, use a dotted name such as
Status.SHIPPEDor a literal, never a bare name.
Exercise
Exercise · Easy · Python
Answer a music player's commands with match
A music player has a text box for commands. Write respond(command), which returns the player's reply to one command, using a match statement (the tests check that there is one).
First remove the spaces at both ends of the command and make it lower case, so that " Play " counts as "play". Then reply:
- to
play:Playing - to
pauseorstop:Paused - to
nextorskip:Next song - to
helpor?:Commands: play, pause, next, help, or a volume from 0 to 100 - to a whole number from 0 to 100, such as
35:Volume 35 - to anything else:
Unknown command: 'dance', with the cleaned command in quotes asrepr()writes it
So respond("SKIP") returns "Next song", respond("101") returns "Unknown command: '101'" and respond("") returns "Unknown command: ''".
Starter code · player.py
def respond(command):
"""Return the music player's reply to one typed command."""
text = command.strip().lower()
match text:
case "play":
return "Playing"
case _:
return f"Unknown command: {text!r}" The sample tests · test_player.py
import ast
from pathlib import Path
from player import respond
HELP = "Commands: play, pause, next, help, or a volume from 0 to 100"
def test_play():
"""replies Playing to play"""
assert respond("play") == "Playing"
def test_pause_and_stop():
"""treats pause and stop the same"""
assert respond("pause") == "Paused"
assert respond("stop") == "Paused"
def test_next_and_skip():
"""treats next and skip the same"""
assert respond("next") == "Next song"
assert respond("skip") == "Next song"
def test_help():
"""lists the commands for help and ?"""
assert respond("help") == HELP
assert respond("?") == HELP
def test_spaces_and_case():
"""ignores spaces at both ends and upper case letters"""
assert respond(" Play ") == "Playing"
assert respond("SKIP") == "Next song"
def test_volume():
"""sets the volume for a whole number from 0 to 100"""
assert respond("35") == "Volume 35"
assert respond("0") == "Volume 0"
assert respond("100") == "Volume 100"
def test_volume_out_of_range():
"""does not set a volume above 100 or below 0"""
assert respond("101") == "Unknown command: '101'"
assert respond("-5") == "Unknown command: '-5'"
def test_unknown():
"""names a command it does not know"""
assert respond("dance") == "Unknown command: 'dance'"
assert respond("") == "Unknown command: ''"
assert respond(" Dance ") == "Unknown command: 'dance'"
def test_uses_match():
"""uses a match statement"""
source = Path(__file__).with_name("player.py").read_text(encoding="utf-8")
assert any(isinstance(node, ast.Match) for node in ast.walk(ast.parse(source))) A hint
Put the cleaned text in a variable and match on it: text = command.strip().lower(). A command with two spellings is one OR pattern, such as case "pause" | "stop":. For the volume, capture the text with a name and add a guard: "35".isdecimal() is True and "-5".isdecimal() is False, so check the text before you call int() on it.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Check yourself
5 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
References
- The Python Tutorial: match statements (Python Software Foundation)
- The Python Language Reference: the match statement (Python Software Foundation)
- PEP 634: Structural Pattern Matching: Specification (Python Software Foundation)
- PEP 636: Structural Pattern Matching: Tutorial (Python Software Foundation)
- What's New In Python 3.10: structural pattern matching (Python Software Foundation)
- enum: StrEnum (Python Software Foundation)
- keyword, testing for Python keywords (Python Software Foundation)
- Lexical analysis: soft keywords (Python Software Foundation)
- RFC 9110: HTTP Semantics, section 15 (status codes) (Internet Engineering Task Force (IETF))
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress