Skip to content
← All Python articles

Python explainer

Branch on a Value with match and case

About 3 min to read. Allow extra time to work through examples.

Replace a chain of if and elif with match and case. Learn literal patterns, the wildcard, capture patterns, type checks and guards, and why a bare name in a case never compares.

This article uses Python. You can make it your practice mode to browse related content across the site.

Your app reads a command from a form field and decides what to do. The usual code compares one value again and again:

def handle(command: str) -> str:
    if command == "start":
        return "Starting"
    elif command == "stop" or command == "quit":
        return "Stopping"
    else:
        return f"Unknown: {command}"

A match statement says the same thing as a list of cases.

One match, several cases

def handle(command: str) -> str:
    match command:
        case "start":
            return "Starting"
        case "stop" | "quit":
            return "Stopping"
        case _:
            return f"Unknown: {command}"

The value after match is the subject. Each case holds a pattern. Python tries the cases from top to bottom. It runs the body of the first case whose pattern matches and skips the rest. If no case matches, nothing happens, and the code after the match runs.

"start" is a literal pattern. It matches when the subject equals "start". "stop" | "quit" matches either string. _ is the wildcard. It matches anything and stores nothing, so it works like else at the end.

Store what you matched

A bare name is a capture pattern. Like _, it matches anything, and it also stores the subject in that name:

        case other:
            return f"Unknown: {other}"

A type name followed by () checks the type, the way isinstance does. Add as and a name to keep the value:

def describe(value: int | str) -> str:
    match value:
        case int() as n:
            return f"number {n}"
        case str() as text:
            return f"text {text!r}"

This is a class pattern. Later exercises use it with your own classes, where it can also pull out attributes. mypy uses the check too. Inside the first case, it knows that n is an int.

Add a condition with a guard

A guard is an if condition after the pattern. The case matches only when the pattern matches and the condition is true. Otherwise Python moves on to the next case.

def status_label(code: int) -> str:
    match code:
        case 200:
            return "ok"
        case 404 | 410:
            return "gone"
        case int() as n if n >= 500:
            return "server error"
        case _:
            return "other"


print(status_label(503))  # server error
print(status_label(302))  # other

A bare name never compares

This looks as if it checks the status against OK:

OK = 200


def label(status: int) -> str:
    match status:
        case OK:
            return "ok"
    return "not ok"

It does not. OK is a capture pattern, so it matches every status and stores it in a new local variable called OK. label(404) returns "ok".

To compare, write the literal, as in case 200:. Or use a dotted name. A name with a dot in it, such as an enum member, is a value that Python looks up and compares with the subject:

from enum import IntEnum


class Status(IntEnum):
    OK = 200
    NOT_FOUND = 404


def label(status: int) -> str:
    match status:
        case Status.OK:
            return "ok"
        case Status.NOT_FOUND:
            return "missing"
        case _:
            return "other"

Python catches part of this mistake for you. A capture pattern with no guard matches everything, so it has to be the last case. If another case comes after it, the file does not run at all:

SyntaxError: name capture 'OK' makes remaining patterns unreachable
Check your understanding The last case of a match on an int is case MAX_RETRIES:, where MAX_RETRIES = 3 is a module constant. What does that case match? Show answer

Every value. A bare name is a capture pattern, so the case matches any subject and stores it in a new local variable called MAX_RETRIES. To compare with the constant, write case 3:, use a dotted name such as Limits.MAX_RETRIES, or add a guard, as in case n if n == MAX_RETRIES:.

Going further

Python 3.10 added match. The next exercises add patterns for lists, such as [first, *rest], for dictionaries, such as {"type": "user"}, and class patterns that read attributes from your own classes. The pattern matching tutorial walks through every kind of pattern.

step 7 of 23

Pattern Matching, f-Strings and Newer Syntax

Up next Answer Chat Commands with match and case