Control flow · about 40 min

Structural pattern matching with match

Why this matters

Programs constantly need to react to the shape of data: a command typed by a user (move north, take lamp, quit), a message from a network (["ping"], ["data", payload]), a point that might be on an axis. With if/elif you write a test for the length, then index into the parts, then compare, again and again. Python 3.10 added the match statement, which checks the shape of a value and pulls out its parts in one step. For simple value comparisons it is a tidy alternative to elif chains; for structured data it is much clearer.

A first match

command = input()
match command:
    case "start":
        print("starting")
    case "stop":
        print("stopping")
    case _:
        print(f"unknown command: {command}")

With input stop, this prints stopping.

The parts:

Literal patterns

Numbers, strings, True, False and None written directly in a pattern are literal patterns. They match a subject that is equal (with ==; for True, False and None, with is).

code = int(input())
match code:
    case 200:
        print("OK")
    case 404:
        print("Not Found")
    case _:
        print("something else")

Or-patterns

| combines alternatives; the case matches if any of them does:

day = input().lower()
match day:
    case "saturday" | "sunday":
        print("weekend")
    case "monday" | "tuesday" | "wednesday" | "thursday" | "friday":
        print("weekday")
    case _:
        print("not a day")

So far this is just a neater elif chain. The real power comes next.

Capture patterns

A bare name in a pattern is a capture pattern: it matches anything, and binds that name to the value it matched.

match input():
    case "quit":
        print("bye")
    case other:
        print(f"you said {other!r}")

With input hello, it prints you said 'hello'. other works like _ except that it keeps the value. (_ is special: it never binds.)

Warning: Because a bare name captures, you cannot compare with a variable by writing its name. case QUIT: with QUIT = "quit" defined earlier does not test equality; it matches everything and rebinds QUIT. Use the literal case "quit":, a guard (case x if x == QUIT:), or a dotted name like case config.QUIT: (dotted names are treated as values to compare with, not captures).

Sequence patterns

A pattern in square brackets (or parentheses) matches a sequence of a specific length and matches each element against a sub-pattern. This is where match shines. Combined with str.split(), which turns a line into a list of words, you can parse commands elegantly:

line = input()
match line.split():
    case ["go", direction]:
        print(f"you walk {direction}")
    case ["take", item]:
        print(f"you pick up the {item}")
    case ["look"]:
        print("you see a dark room")
    case []:
        print("say something")
    case _:
        print("I don't understand")
InputWordsMatching caseOutput
go north['go', 'north']["go", direction]you walk north
take lamp['take', 'lamp']["take", item]you pick up the lamp
look['look']["look"]you see a dark room
(empty line)[][]say something
go['go']_I don't understand

Each sequence pattern checks three things at once: that the subject is a sequence, that it has the right length, and that each element matches its sub-pattern (a literal like "go" must be equal; a name like direction captures). Writing that with if statements takes noticeably more code:

words = line.split()
if len(words) == 2 and words[0] == "go":
    direction = words[1]
    print(f"you walk {direction}")
elif ...

Note: Strings are sequences too, but sequence patterns deliberately do not match str, bytes or bytearray subjects; otherwise case [a, b]: would match every two-character string, which is rarely what you mean. Lists and tuples (and ranges) are matched.

Star patterns: variable length

*name inside a sequence pattern matches zero or more elements, binding them as a list:

match input().split():
    case ["echo", *words]:
        print(" ".join(words))
    case [first, *_, last]:
        print(f"from {first} to {last}")
    case _:
        print("too short")
InputOutput
echo hello therehello there
echo(an empty line: words is [])
a b c dfrom a to d
xtoo short

At most one star is allowed per sequence pattern. *_ matches any number of elements without binding them.

Matching tuples

A pair of values in parentheses, like (x, y), is a tuple: a fixed sequence (covered properly in the collections module). Matching on a tuple lets you test several values at once:

match (x, y):
    case (0, 0):
        print("origin")
    case (a, 0):
        print(f"on the x-axis at {a}")
    case (0, b):
        print(f"on the y-axis at {b}")
    case (a, b):
        print(f"at {a}, {b}")

Here literals and captures mix freely: (a, 0) means "any pair whose second element equals 0; call the first one a".

Guards

A pattern can be followed by if condition, called a guard. The case matches only if the pattern matches and the guard is truthy. Names captured by the pattern can be used in the guard:

match input().split():
    case ["withdraw", amount] if int(amount) > 500:
        print("limit exceeded")
    case ["withdraw", amount]:
        print(f"dispensing {amount}")
    case _:
        print("invalid")

With withdraw 800: limit exceeded. With withdraw 40: dispensing 40. If the guard fails, Python moves on and tries the next case, so order the specific (guarded) case before the general one.

How Python matches, underneath

match is not a jump table. Python compiles it into a sequence of tests, tried in order, just like an if/elif chain but generated for you: for a sequence pattern, check that the subject is a sequence (and not a string), check its length, then compare or bind each element. There are dedicated bytecode instructions for parts of this (MATCH_SEQUENCE, GET_LEN), visible with dis. So performance is similar to the equivalent if chain; the gain is clarity, not speed.

There are more pattern kinds that you will be ready for later: mapping patterns for dictionaries (case {"type": "click", "x": x}:) and class patterns for objects (case Point(x=0, y=0):), plus as to capture a sub-pattern (case ["go", ("north" | "south") as d]:).

When to use match

Common mistakes

Recap

Going further: read PEP 636, "Structural Pattern Matching: Tutorial" (peps.python.org/pep-0636), which builds a text adventure step by step and covers mapping and class patterns.

Exercises

Exercise 1 · write code

HTTP status classes

Read an HTTP status code (a whole number) and print its meaning using a match statement:

codeoutput
200OK
201Created
301 or 302Redirect
404Not Found
500, 502 or 503Server Error
anything elseUnknown

File: main.py

Starting point

code = int(input())
match code:
    case 200:
        print("OK")

Example

input

200

expected output

OK

Example

input

201

expected output

Created
Hint 1

Combine alternatives in one case with |: case 301 | 302:.

Hint 2

case _: matches anything and must come last.

Reference solution
code = int(input())
match code:
    case 200:
        print("OK")
    case 201:
        print("Created")
    case 301 | 302:
        print("Redirect")
    case 404:
        print("Not Found")
    case 500 | 502 | 503:
        print("Server Error")
    case _:
        print("Unknown")

Exercise 2 · quiz

The capture-pattern trap

What does this print?

QUIT = "quit"
command = "help"
match command:
    case QUIT:
        print("bye")
print(QUIT)
Answer
  • bye, then help

A bare name in a pattern is a capture pattern: it matches anything and binds that name to the subject. case QUIT: does not compare with the variable QUIT; it matches "help", rebinds QUIT to "help", and prints bye. The final line then prints the new value, help. (If any other case followed it, Python would refuse the file with SyntaxError: name capture 'QUIT' makes remaining patterns unreachable.) To compare with a constant, use a literal (case "quit":), a guard, or a dotted name such as case Commands.QUIT:.

Exercise 3 · write code

A command calculator

Read lines until a line that is exactly quit. Each line is a command, words separated by spaces. Use match on line.split() to handle:

commandoutput
add X Ythe sum of the whole numbers X and Y
mul X Ytheir product
neg XX negated
sum followed by any number of whole numbers (possibly none)their sum
anything elseerror: followed by the original line

For this input:

add 2 3
neg 7
sum 1 2 3 4
sum
mul 6 7
dance
quit

the output must be exactly:

5
-7
10
0
42
error: dance

A sequence pattern like case ["add", x, y]: matches a list of exactly three words whose first is "add", binding x and y to the other two (as strings, so convert with int). case ["sum", *rest]: binds rest to a list of the remaining words; loop over it with for.

File: main.py

Starting point

while True:
    line = input()
    if line == "quit":
        break
    match line.split():
        case ["add", x, y]:
            print(int(x) + int(y))

Example

input

add 2 3
neg 7
sum 1 2 3 4
sum
mul 6 7
dance
quit

expected output

5
-7
10
0
42
error: dance

Example

input

add 1
neg 1 2
quit

expected output

error: add 1
error: neg 1 2
Hint 1

Add one case per command, plus case _: for errors.

Hint 2

case ["sum", *rest]: matches sum with zero or more following words.

Hint 3

Patterns are tried in order, so ["add", x, y] will not match add 1 (wrong length); that falls through to case _.

Reference solution
while True:
    line = input()
    if line == "quit":
        break
    match line.split():
        case ["add", x, y]:
            print(int(x) + int(y))
        case ["mul", x, y]:
            print(int(x) * int(y))
        case ["neg", x]:
            print(-int(x))
        case ["sum", *rest]:
            total = 0
            for word in rest:
                total += int(word)
            print(total)
        case _:
            print(f"error: {line}")

Exercise 4 · write code

Points on a plane

Read two whole numbers on one line, x y, the coordinates of a point. Use match on the pair (x, y) with literal patterns, capture patterns and guards (if ... after a pattern) to print:

The rules are checked in that order, so (0, 0) is origin, not "on the diagonal". Read the input with:

parts = input().split()
x = int(parts[0])
y = int(parts[1])

and then write match (x, y): (the parentheses make a pair, called a tuple, which a sequence pattern like case (0, 0): or case (a, 0): can match).

File: main.py

Starting point

parts = input().split()
x = int(parts[0])
y = int(parts[1])
match (x, y):
    case (0, 0):
        print("origin")

Example

input

0 0

expected output

origin

Example

input

-3 0

expected output

on the x-axis at -3
Hint 1

case (a, 0): matches any pair whose second element is 0, binding the first to a.

Hint 2

A guard adds a condition: case (a, b) if a == b:.

Hint 3

Once the axes and diagonal are handled, the quadrant cases only need sign tests.

Reference solution
parts = input().split()
x = int(parts[0])
y = int(parts[1])
match (x, y):
    case (0, 0):
        print("origin")
    case (a, 0):
        print(f"on the x-axis at {a}")
    case (0, b):
        print(f"on the y-axis at {b}")
    case (a, b) if a == b:
        print("on the diagonal")
    case (a, b) if a > 0 and b > 0:
        print("quadrant 1")
    case (a, b) if a < 0 and b > 0:
        print("quadrant 2")
    case (a, b) if a < 0 and b < 0:
        print("quadrant 3")
    case _:
        print("quadrant 4")