Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Beginner
  • 18 minutes
  • Examples run with Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot

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.

A match statement evaluates its subject once, then tries each case from the top and runs the first whose pattern fits and guard is true.match subject:evaluated onceTake the next case,from the topPatternfits?Guard true,or no guard?Run its block,skip all othersNo case left:nothing runsyesnoyesnonone left

How a match statement chooses a case

Text description of the diagram

The diagram is a flow chart that runs from top to bottom.

  1. Python evaluates the subject, the expression after match, once.
  2. It takes the next case, starting with the first one.
  3. If that case's pattern does not fit the subject, it goes back to step 2 with the case after it.
  4. 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.
  5. If the guard is true, Python runs that case's block and skips every other case.
  6. 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:

A calculator that matches the operator Python · calculator.py
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

Three kinds of pattern are at work:

  • case "+": is a literal pattern. It fits when operator == "+".
  • 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 last case _ handles everything the cases above it did not. Here it raises a ValueError that 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:

A match with no case for the rest Python · sizes.py
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

"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:

HTTP status codes with captures and guards Python · http_status.py
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

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.

HTTP Status Codes Reference Look up what each HTTP status code means and when servers send it.

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:

A constant used as a pattern Python · capture_trap.py
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

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:

Python stops the trap when cases follow it Python · capture_error.py
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

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:

Comparing with named constants Python · status_enum.py
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

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:

match is also an ordinary name Python · soft_keywords.py
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

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.

Python Online Compiler Copy the calculator into the compiler and add a case for the power operator, **.

Key takeaways

  • match evaluates the subject once and runs the first case whose pattern fits and whose guard is true; there is no fall-through and no break.
  • Literal patterns compare with == (True, False and None with is), | gives alternatives, and case _ 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.SHIPPED or 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 pause or stop: Paused
  • to next or skip: Next song
  • to help or ?: 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 as repr() 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.

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.

  1. Question 1 of 5 What does capture_trap.py print?

    What does this program print? Choose one answer.

    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))
    Show the answer to question 1

    Answer: it prints

    packed -> being packed
    shipped -> on its way
    cancelled -> on its way
    SHIPPED is now 'cancelled'

    case SHIPPED: is a capture pattern: it matches any subject and assigns it to the name SHIPPED. So "cancelled" is reported as on its way, and because the loop runs at the top level of the file, the constant itself now holds the last status, 'cancelled'.

  2. Question 2 of 5 What does this print?

    Read the code, then choose one answer.

    match 7:
        case 1 | 2 | 3:
            print("small")
        case n if n % 2 == 0:
            print("even", n)
        case n:
            print("odd", n)
    Show the answer to question 2

    Answer: odd 7

    7 is not 1, 2 or 3, so the OR pattern fails. The second case captures 7 in n, but its guard n % 2 == 0 is false, so Python moves on. The last case is a capture pattern with no guard, which matches anything.

  3. Question 3 of 5 A file defines SHIPPED = "shipped" and class Status(StrEnum) with the member SHIPPED = "shipped". Which case runs only when the subject equals "shipped"?

    Choose one answer.

    Show the answer to question 3

    Answer: case Status.SHIPPED:

    A dotted name is a value pattern: Python looks it up and compares it with ==. A bare name such as SHIPPED or status is a capture pattern and matches anything, and _ is the wildcard, which also matches anything.

  4. Question 4 of 5 Which of these statements about Python's match statement are true?

    Choose every answer that is right.

    Show the answer to question 4

    Answer:

    • Only the block of the first case that matches runs
    • When no case matches and there is no case _, nothing runs and no error is raised

    Python tries the cases in order and runs one block at most; there is no fall-through, so no break is needed. If nothing matches, the statement simply ends. The wildcard _ matches anything but binds no name.

  5. Question 5 of 5 In case code if 400 <= code <= 499:, when does Python evaluate the guard?

    Choose one answer.

    Show the answer to question 5

    Answer: After the pattern has matched and the subject has been bound to code

    A guard is checked only for a case whose pattern has already matched, so the names the pattern captured can be used in it. If the guard is false, Python tries the next case.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.