Embercode
✕
ConditionalsLesson 10 of 14

35 min read

Shared cases in match with no fall-through

Why this matters

A match statement picks one block of code from several options, based on one value. Each option starts with a case line, such as case 2:. Python compares the value after match with each case in turn. The first case that fits runs, and the statement ends.

Many languages, such as C and Java, work differently. In those languages a matching case keeps running into the cases below it until a break stops it. Running from one case into the next is called fall-through. A reader who knows those languages will look for it here. Python's match has no fall-through, so a missing line never makes a case run into the next one.

This lesson shows what Python does in its place. The sections cover sharing one block between several values, writing shared work out in full, and the mistakes that still happen. The programs were run with Python 3.14.4. The match statement needs Python 3.10 or later.

The idea: one case runs, then the match ends

Start with a plain match. Read it once without running it and try to predict the output.

Python · editable

Output:

standard
done

The value level is 2, so Python runs the block under case 2:. It prints standard. The block then ends, and the match ends with it. The other blocks are skipped. The line that prints done is after the match, so it always runs.

Nothing needs to stop the block. A block that finishes is the end of the match, so there is no break line to forget.

A break inside a loop ends the loop

A break is allowed in a match only when the match sits inside a loop. In that case the break ends the loop, not just the match. The next program has a match inside a for loop.

Python · editable

Output:

value 0
after the match, i = 0
found one, leaving the match

When i is 1, the break ends the for loop. The loop never reaches i = 2, and the print after the match does not run for i = 1. To leave only the match and keep the loop going, put the rest of the work in an if block instead of using break.

A break outside any loop is a mistake. The next program has one.

match 2:
    case 2:
        print("two")
        break

Python refuses the file before it runs:

  File "main.py", line 4
    break
    ^^^^^
SyntaxError: 'break' outside loop

The path in the first line depends on where the file is saved.

Case stacking with |

In some languages, several labels can sit one above another with no code between them. They then share the code under them. In Python, the same result comes from a pattern that joins the values with |, which means "or".

Python · editable

Output:

day 1 is a weekend
day 2 is a weekday
day 3 is a weekday
day 4 is a weekday
day 5 is a weekday
day 6 is a weekday
day 7 is a weekend

The pattern 1 | 7 fits when the value is 1 or 7. Both values run the same block. Because the pattern is a single pattern, no value falls into a block below it, so there is nothing for a reader to check.

Planned sharing: write the shared work out

Some programs want a higher level to get everything that the lower levels get. In a language with fall-through, the code for the top level runs into the code below it. Python has nothing that runs into another case, so the shared work has to be written in a way that makes the sharing clear.

The first way is to keep the work in a list and take the first few items. A slice is a part of a list. PERKS[:level] means the first level items of PERKS.

Python · editable

Output:

level 1 gets 1 perks: ['email support']
level 2 gets 2 perks: ['email support', 'extra storage']
level 3 gets 3 perks: ['email support', 'extra storage', 'priority support']
level 4 gets 0 perks: []

The check level in (1, 2, 3) looks at a tuple, which is a fixed list written in parentheses. Level 4 is not in the tuple, so it gets no perks. Each higher level gets the lower levels' perks because the slice starts at the first item every time.

Worked example: plan tiers

Here is a function for three plans. Each higher plan includes the features of the plans below it. The plans basic and trial share one line of code.

The first version writes each plan's full list in its own block.

Python · editable

Output:

pro: ['priority support', 'extra storage', 'email login']
plus: ['extra storage', 'email login']
trial: ['email login']
Traceback (most recent call last):
  ...
    print("gold:", features_for("gold"))
  ...
    raise ValueError(f"unknown tier: {tier}")
ValueError: unknown tier: gold

The lines between the first line and the last line are shortened with ..., because the file paths depend on the machine. raise stops the function and reports the error.

Each block returns its full list. The pro block repeats the lines that plus and basic also have. That repetition is the cost of having no fall-through, and it is easy to get wrong when a line is changed in one block and not the others.

The version below builds each list from the list below it. The + operator joins two lists into one new list.

Python · editable

Output:

pro: ['priority support', 'extra storage', 'email login']
plus: ['extra storage', 'email login']
trial: ['email login']

The function returns list(PRO) rather than PRO itself. list(...) makes a copy. Without the copy, a caller who added an item to the returned list would change PRO for every later call.

Common mistakes

A missing break does not change the result

A match has no break to forget. The next program has no break after one. In Python, it prints one only.

Python · editable

Output:

one

When you port a switch that depended on fall-through, the output changes, because the lower cases no longer run. Find the cases that the code meant to share. Then write them with |, or write out their work in each block, as in the plan tiers example.

A name set in one case is not set in another

Python does not give each block its own scope. A name that is set in one block belongs to the whole function. Reading that name in a block that did not set it causes an error when that block runs. The next program sets y in one case and reads it in another.

Python · editable

Output:

5
Traceback (most recent call last):
  ...
    run(2)
  ...
    print(y)
          ^
UnboundLocalError: cannot access local variable 'y' where it is not associated with a value

The first call prints 5. The second call reads y before any line in that call has set it. Python reports this only when the line runs, so a test has to reach that line to find the problem. The fix is to set the name before the match, or to put all the work for a case in that case's block.

A repeated pattern is not an error

Python does not check for repeated patterns. In the next program, the second case 1 never runs, and no message appears.

Python · editable

Output:

one

To fix it, remove the second pattern, or join the two with | in one case.

A wildcard above other cases stops the program

The _ pattern fits any value, so it must be the last case. Python refuses a file where any case comes after it.

code = 2
match code:
    case _:
        print("other")
    case 2:
        print("two")

Python refuses the file before it runs:

  File "main.py", line 3
    case _:
         ^
SyntaxError: wildcard makes remaining patterns unreachable

Reading a match you did not write

When a match is new to you, read it in three passes. Together they show where each value's work comes from.

The first pass finds each case block. Cases that share a block are joined with | on one line, and they need no further check.

The second pass checks that _ is the last case, if there is one. Python refuses the file otherwise, so this is a quick check.

The third pass reads each block to its end and asks what it does for every value in its pattern. A block never hands work to the block below it. If one value should get the work of another value, the code must call a function, read a shared list, or repeat the lines. If none of those is there, the author has to decide whether the missing work was intended.

Practice

Write each answer in a file, run it with python3 file.py, and compare the result with the answer.

  1. Predict the output of this program before you run it.
n = 4
match n:
    case 4:
        print("a", end="")
    case 5:
        print("b", end="")
    case 6:
        print("c", end="")
print("d")

Show the answer

The output is ad on one line. The value 4 runs the block under case 4:, which prints a. The end="" part of print stops it from adding a newline. There is no run into case 5:, so b is not printed. The print("d") after the match prints d and a newline.

  1. Write a function box_message(size) that returns small box for small, medium box for medium, and large box for large or xl. Any other size returns unknown size. Use one block for large and xl.

Show the answer

def box_message(size):
    match size:
        case "small":
            return "small box"
        case "medium":
            return "medium box"
        case "large" | "xl":
            return "large box"
        case _:
            return "unknown size"

box_message("small") returns small box only. The medium block never runs for small, and no break is needed.

  1. Write a function days_in(month, leap_year) that returns the number of days in a month. Months 1, 3, 5, 7, 8, 10 and 12 have 31 days. Months 4, 6, 9 and 11 have 30 days. February has 29 days in a leap year and 28 days otherwise. Any other month raises a ValueError. Use | for each group of months.

Show the answer

def days_in(month, leap_year):
    match month:
        case 1 | 3 | 5 | 7 | 8 | 10 | 12:
            return 31
        case 4 | 6 | 9 | 11:
            return 30
        case 2:
            return 29 if leap_year else 28
        case _:
            raise ValueError(f"bad month: {month}")

days_in(3, False) returns 31, days_in(9, False) returns 30, and days_in(2, True) returns 29. days_in(13, False) raises ValueError: bad month: 13.

  1. Write a function is_vowel(c) that returns True for a, e, i, o and u, in either upper or lower case. Use .lower() and |.

Show the answer

def is_vowel(c):
    match c.lower():
        case "a" | "e" | "i" | "o" | "u":
            return True
        case _:
            return False

is_vowel("E") returns True, and is_vowel("t") returns False. Lowering the letter first means the patterns only need lowercase letters.

  1. A teammate changes the _ block of a match on Light to pass, so that the function never raises an error. Explain in plain words what this costs.

Show the answer

A member added to Light later falls into the _ block, does nothing, and the program keeps going with no sign of a problem. Keep an error in the _ block, for example by calling assert_never(light), so that a new member stops the program at once.

Summary

  • A Python match runs the block of the first case that fits, then ends. Blocks never run into the block below them.
  • Joining values with | in one case lets them share one block, with no fall-through to check.
  • The work that another language would get from fall-through must be written out, kept in a list, or put in a function.
  • A break ends the loop around a match, and a break outside any loop is a syntax error.
  • A repeated case is not an error, a _ case above other cases is an error, and a name set in one case may be unset in another.
Check yourself: Switch (8 questions) →Practice this topic: coding problems for Conditionals →

Stuck? Ask the tutor

Ask about anything in this lesson. Answers are short and use simple words.

Sign in to ask the tutor.

← Match on strings and enums