Embercode
✕
ConditionalsLesson 9 of 14

40 min read

Match on strings and enums

Why this matters

Many programs compare one value against a list of known values. A chat program reads a command such as start or stop. A shipping program maps a status such as SHIPPED or DELIVERED to a message. A calendar program maps a day of the week to a number.

An if chain can do this, but a long chain repeats the same variable name over and over and is hard to read. A match statement puts the value at the top and lists each possible match as a block of code under a case line.

This lesson covers match on two kinds of value. A string is a piece of text, such as "start". An enum (short for enumeration) is a class that lists a fixed set of named values, such as the seven days of the week. The enum module in the standard library builds these classes.

The text after case is called a pattern. For a plain value such as "start", a pattern means "the value is equal to this". The match statement was added in Python 3.10. The programs in this lesson were run with Python 3.14.4.

Readers who know Java will notice that Python's match is a statement, not a value, and that its blocks never run into the next one, so no break is needed. Both points are covered below.

Start with a string match

Here is a match on a string. The function takes a command and returns a reply.

Python · editable

Output:

starting
stopping
unknown: Start
unknown: pause

Python takes the value after match, which is text, and compares it with each pattern in turn. When a pattern fits, Python runs the block under it and the match ends. The _ pattern is the fallback. It fits any value, so its block runs only when no other pattern fits. It works like default in other languages.

The f"unknown: {text}" line is an f-string. The letter f before the quotes tells Python to put the value of text into the text where the braces are.

The comparison is exact. The text "Start" with a capital S does not equal "start", so it reaches _. Python compares text letter by letter, and capital and small letters are different letters. If the program must accept both, change the text first, as Mistake 1 below shows.

Labels must be fixed values

A bare name after case, such as target, is not compared with a variable. Python reads it as a capture pattern. A capture pattern fits any value and gives that value the name. This is a trap for people who expect a comparison.

The next program has a variable called target. The variable text holds stop, but the first case still runs.

Python · editable

Output:

match
stop

The capture pattern target fit the value stop and stored it in the name target. The original "start" is gone. Adding a case _: after a capture pattern makes Python refuse the whole file before it runs. Python checks this when it reads the file, so the error appears with no output at all.

  File "main.py", line 4
    case target:
         ^^^^^^
SyntaxError: name capture 'target' makes remaining patterns unreachable

The path in the first line depends on where the file is saved, and the traceback lines in this lesson that show paths are shortened with .... The message means that every case after case target: could never run.

To compare with a fixed value, write a dotted name such as Mode.START. A dotted name is compared with the value. The next program puts the values in a class, which is a simple way to group names.

Python · editable

Output:

stopping

A plain variable name after case is never a comparison, so when the value to compare with is held in a variable, use if and == instead.

Enums with match

An enum is a class whose members are fixed. The next program defines an enum Day with the seven days. auto() gives each member a number, so the code does not have to choose them. The function day_number turns a day into its position in the week. The function kind says whether the day is a weekday or a weekend day.

Python · editable

Output:

MONDAY -> 1 weekday
TUESDAY -> 2 weekday
WEDNESDAY -> 3 weekday
THURSDAY -> 4 weekday
FRIDAY -> 5 weekday
SATURDAY -> 6 weekend
SUNDAY -> 7 weekend
almost weekend
6

Several things happen in this program.

The patterns for enum members have the class name in front, such as Day.MONDAY. This is the dotted form from the section above. A bare MONDAY would be a capture pattern, and it would match every day.

The pattern Day.SATURDAY | Day.SUNDAY joins two patterns with |, which means "or". The block runs when either one fits. This is how one block serves two values.

The function day_number returns from inside each block. A match statement does not produce a value that can be stored, so the value is returned from the function. Mistake 4 shows what happens when the value is written on the right side of =.

Day["FRIDAY"] finds the member whose name is FRIDAY. Mistake 6 shows what happens when the name does not exist.

The for d in Day: loop visits the members in the order they are written in the class. d.name gives the member name as text.

The last line counts positions from 0. list(Day) makes a list of all the members in order, and .index(...) gives the position of one of them. SUNDAY is in position 6, so the output is 6.

Example: a file type lookup

The next example uses a string with several patterns for each result. A file name's extension is the text after the last dot, such as png in photo.png. The function below maps an extension to a category.

Python · editable

Output:

image document other

The _ case is what gives an answer for every text that is not listed. Without it, the function would return None for "exe", which is a value that means "nothing was returned".

Enum matches do not check every value

An enum has a fixed list of members, but Python does not check a match against that list. The program below handles two of the three Light members. It runs without any error or warning.

Python · editable

Output:

go

The call action(Light.YELLOW) prints nothing. No pattern matches YELLOW, so the match does nothing and the program goes on without a sign of a problem.

To make a missing member stop the program, add a case _: block that calls assert_never from the typing module. The function raises an error when Python reaches it. The next program adds a member BLUE to Light but leaves it out of action.

Python · editable

Output:

go
Traceback (most recent call last):
  ...
  File "/usr/lib/python3.14/typing.py", line 2582, in assert_never
    raise AssertionError(f"Expected code to be unreachable, but got: {value}")
AssertionError: Expected code to be unreachable, but got: <Light.BLUE: 4>

The lines between the first and the last File line are left out here. The paths in a traceback depend on the machine. The last line names the value that reached the case _: block, so the missing member is visible.

Some type checkers, which are tools that read code without running it, can report a match that misses a member when assert_never is used. This lesson does not run a type checker.

None values

Python's None means "no value at all". A match can have a pattern for it, written as case None:. Without that pattern, a None value does not stop a match. It simply does not equal any string, so it reaches _.

The next program adds the None pattern and runs three checks.

Python · editable

Output:

no input
starting
stopping
unknown: STOP

The None pattern runs before any text comparison. The STOP line shows the case-sensitive match again. "STOP" does not equal "stop", so it reaches _.

The error appears later, when a method is called on None. The next program calls .lower(), which is a method that works only on text.

Python · editable

Output:

Traceback (most recent call last):
  ...
    return text.lower()
           ^^^^^^^^^^
AttributeError: 'NoneType' object has no attribute 'lower'

The message says that the value's type is NoneType, which is the type of None, and that it has no attribute called lower. An attribute is a name that belongs to a value, such as a method.

Common mistakes

Mistake 1: forgetting that matching is case-sensitive

The text "Start" does not match the pattern "start". The first example in this lesson shows this. If a user can type either case, change the text before the match, for example with text.lower(), and then compare with lowercase patterns.

match text.lower():
    case "start":
        ...

Mistake 2: passing None to a function that expects text

A match with a case _: never fails on None. The failure comes from the code inside a block, such as the .lower() call in the section above. The fix is a case None: pattern, or a check before the call.

Mistake 3: repeating a pattern

Python does not stop a repeated pattern. The first block that fits runs, and the later one never does. The next program has "start" twice.

Python · editable

Output:

starting

No error is shown. The second "start" block is dead code (code that can never run). Remove it, or merge the two patterns into one, such as case "start" | "begin":.

Mistake 4: using match as a value

A match is a statement. It cannot be written on the right side of =. The next program tries it.

s = "a"
n = match s:
    case "a":
        1
    case "b":
        2
print(n)

Python refuses the file before it runs:

  File "main.py", line 2
    n = match s:
              ^
SyntaxError: invalid syntax

To get a value, return it from a function in each block, as in the day_number example. For a small table of values, a dictionary works. A dictionary maps each key to a value and is written in braces, such as {"a": 1}. The .get method returns the value for a key, or a default when the key is missing.

Python · editable

Output:

1

Mistake 5: a match that silently skips a value

The enum section shows a match that prints go only, because the call with YELLOW matched no pattern. Python does not warn about this. When every member must be handled, add a case _: that raises an error, such as the assert_never call above. When a value should be handled by doing nothing, write a case _: with pass (a statement that does nothing) and a comment that says so.

Mistake 6: converting text to an enum member that does not exist

Light["BLUE"] looks up a member by its name, and the name must match exactly. A name that is not in the enum raises an error. Calling the class with a value that is not in the enum raises a different error.

Python · editable

The second line uses "BLUE", which is not a member name. Its output is:

Light.GREEN
Traceback (most recent call last):
  ...
    print(Light["BLUE"])
  ...
KeyError: 'BLUE'

The lines between are left out because the paths depend on the machine. A call such as Light(9), which looks up by value rather than by name, raises ValueError: 9 is not a valid Light instead.

When the text comes from a user, check it first, or catch the error with try and except KeyError (try runs code that may fail, and except runs when it does), so the program can show a friendly reply.

Practice

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

  1. Write a function kind_of(ext) that returns image for png, jpg, gif or image, returns document for pdf or txt, and returns other for anything else. Use a match statement.

Show the answer

def kind_of(ext):
    match ext:
        case "png" | "jpg" | "gif" | "image":
            return "image"
        case "pdf" | "txt":
            return "document"
        case _:
            return "other"
  1. An enum Suit lists HEARTS, DIAMONDS, CLUBS and SPADES. Write a function color(suit) that returns red for hearts and diamonds and black for clubs and spades. Use a match with no case _: block.

Show the answer

from enum import Enum, auto
 
 
class Suit(Enum):
    HEARTS = auto()
    DIAMONDS = auto()
    CLUBS = auto()
    SPADES = auto()
 
 
def color(suit):
    match suit:
        case Suit.HEARTS | Suit.DIAMONDS:
            return "red"
        case Suit.CLUBS | Suit.SPADES:
            return "black"

Every member is listed, so each suit gets a color. If one suit were left out of the match, the function would return None for it, and Python would not report the gap.

  1. The function below has a case for "quit" only. What does run("quit") return, and what does run("Quit") return?
def run(cmd):
    match cmd:
        case "quit":
            return "bye"
        case _:
            return "unknown"

Show the answer

run("quit") returns bye. run("Quit") returns unknown, because the comparison is case-sensitive.

  1. This program does not run. Explain why, and fix it.
s = "a"
n = match s:
    case "a":
        1
    case "b":
        2
print(n)

Show the answer

Python reports SyntaxError: invalid syntax on the line with match. A match is a statement, so it cannot be the value on the right side of =. One fix is a dictionary: n = {"a": 1, "b": 2}.get(s, 0). Another is a function that returns the value from each block.

  1. Change the command function from the None section so that a None input returns no input, and every other input behaves as before. Test it with None, with start and with stop.

Show the answer

def command(text):
    match text:
        case None:
            return "no input"
        case "start":
            return "starting"
        case "stop":
            return "stopping"
        case _:
            return f"unknown: {text}"

Run it with None, start and stop. The three results are no input, starting and stopping.

Summary

  • A match statement compares one value with a list of patterns, and runs the block of the first pattern that fits. Use _ for the rest.
  • String matching is case-sensitive, so "Start" and "start" are different values.
  • A bare name after case captures any value. To compare with a fixed value, use a dotted name such as Light.RED.
  • Python does not check that a match on an enum covers every member. Use assert_never in a case _: block to stop the program when a member is missing.
  • A match is a statement, not a value. A None value does not stop a match, but a method called on None stops the program with AttributeError.
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.

← Range checks in Python