Python Case Statement: How To Create Switch-Case in Python?

Python never shipped a switch statement, so generations of us built our own out of long if-elif chains and dict lookups. I ran every style on Python 3.11.16 this morning, side by side, and the surprise was not speed.

You will see which branch to reach for by the end, and where match breaks when you lean on it.

Python Never Had a Switch Statement Until Match Arrived

Other languages give you switch for free, while Python refused for decades and its tutorial still names the if-elif chain as the substitute.

That changed with Python 3.10 in 2021, which added the match statement through PEP 634, and I confirmed this against the current tutorial because stale posts still claim Python has no answer. That era is over.

Switch compares one value against constants, while match pulls values out of tuples, lists, dicts, and objects, then branches on shape as well as content. The resemblance misleads, because the two only look alike from a distance.

ConstructCompares valuesOpens up dataNeeds a version
If-elif chainYesNoNo, runs everywhere
Dict lookupKeys onlyNoNo, runs everywhere
Match statementYesYes, sequences, mappings, objectsYes, Python 3.10 or newer

What You Need Before You Write Match

Bring Python 3.10 or newer, since older interpreters reject match outright, and I ran everything here on 3.11.16.

Your versionMatch supportWhat to use
3.9 or olderNone, syntax errorIf-elif chains and dict dispatch
3.10 or newerFull statementAny style in this guide
python3 --version
# Python 3.11.16 on the machine used here

If your project still supports 3.9 or older, skip ahead to the if-elif and dict sections, because those run everywhere. Match itself needs no import.

Write Switch-Style Branching Three Ways

One running example carries this whole section, mapping day numbers to names through each style so the outputs show where they agree and diverge.

If-Elif-Else Still Works for Small Chains

The chain below is the style every Python programmer already knows, and I ran it first to set the baseline the other two must beat for clarity.

def check_day(day):
    if day == 1:
        return "Monday"
    elif day == 2:
        return "Tuesday"
    elif day == 3:
        return "Wednesday"
    elif day == 4:
        return "Thursday"
    elif day == 5:
        return "Friday"
    elif day == 6:
        return "Saturday"
    elif day == 7:
        return "Sunday"
    else:
        return "Invalid day"

print(check_day(3))
print(check_day(9))
Wednesday
Invalid day

Nothing here surprises anyone, which is exactly its strength. The weakness shows past a dozen branches, where the chain turns into a wall of repeated comparisons.

Dict Dispatch for Pure Value Lookup

When every branch only returns a value, a dict replaces the whole chain with one lookup. I ran the same inputs through it and got byte-identical answers.

days = {
    1: "Monday",
    2: "Tuesday",
    3: "Wednesday",
    4: "Thursday",
    5: "Friday",
    6: "Saturday",
    7: "Sunday",
}

print(days.get(3, "Invalid day"))
print(days.get(9, "Invalid day"))
Wednesday
Invalid day

The get call carries the default, so the else branch dissolves into one argument. This style breaks down the moment branches need different logic instead of different values.

Match-Case for Branches With Logic

Match earns its keep when one input needs grouping logic, not just a rename. The vertical bar below merges several values into one branch, which neither style above expresses cleanly.

def match_day(day):
    match day:
        case 1:
            return "Monday"
        case 2 | 3 | 4 | 5:
            return "Weekday"
        case 6 | 7:
            return "Weekend"
        case _:
            return "Invalid day"

print(match_day(3))
print(match_day(7))
print(match_day(9))
Weekday
Weekend
Invalid day

I expected a prettier switch and found something closer to a classifier, since the branches describe groups. The underscore in the last branch is the wildcard, and it catches everything the earlier branches decline.

Terminal showing match-case day classifier and guard outputs on Python 3.11
Match output for days and guarded numbers, captured from a live run.

Destructure Sequences and Mappings

No switch statement pulls data apart this way. A case clause names the pieces inside a tuple and binds them to variables on the spot.

def describe(point):
    match point:
        case (0, 0):
            return "origin"
        case (0, y):
            return f"y-axis at {y}"
        case (x, 0):
            return f"x-axis at {x}"
        case (x, y):
            return f"point {x},{y}"
        case _:
            return "not a point"

print(describe((0, 0)))
print(describe((0, 5)))
print(describe((3, 4)))
origin
y-axis at 5
point 3,4

Dicts work the same way, matching keys and capturing the rest. A tiny request router shows the shape.

def route(request):
    match request:
        case {"action": "quit"}:
            return "quitting"
        case {"action": "move", "direction": d}:
            return f"moving {d}"
        case _:
            return "unknown request"

print(route({"action": "move", "direction": "north"}))
print(route({"action": "quit"}))
moving north
quitting

Guards Narrow a Case

A guard adds an if condition to a case so the branch runs only when shape and value agree. Shape alone sometimes matches the wrong branch.

def classify(n):
    match n:
        case x if x < 0:
            return "negative"
        case 0:
            return "zero"
        case x if x % 2 == 0:
            return "positive even"
        case _:
            return "positive odd"

print(classify(-4))
print(classify(0))
print(classify(6))
print(classify(7))
negative
zero
positive even
positive odd

Order decides everything here, because the first matching branch wins and later ones never run. Put the broad check first and the guard below it starves, which the next section demonstrates.

Constants Need Dots and Bare Names Bind

I walked into this trap myself. I assumed a bare name in a case clause compares against my constant, so I wrote it the obvious way and the compiler refused.

NOT_FOUND = 404

def check_const(code):
    match code:
        case NOT_FOUND:
            return "matched constant"
        case _:
            return "default"
SyntaxError: name capture 'NOT_FOUND' makes remaining patterns unreachable

A bare name is a capture, not a comparison, so it grabs the value instead of testing it. The fix is a dotted name, which match treats as a value to compare.

class Codes:
    NOT_FOUND = 404

def check_dotted(code):
    match code:
        case Codes.NOT_FOUND:
            return "matched constant"
        case _:
            return "default"

print(check_dotted(200))
print(check_dotted(404))
default
matched constant

Where Match Breaks and What to Use Instead

Every failure below came out of the interpreter during this refresh. Knowing these edges saves an afternoon.

TrapSymptomFix
Broad branch firstGuard below never firesOrder narrowest to broadest
Bare name in caseCompiler refuses the fileUse a dotted name
Expecting fall-throughLater branches never runRestructure, one branch wins

Broad branches swallow narrow ones. The function below returns the generic answer for 6 because the int check fires first and the guard never gets consulted.

def classify_wrong(n):
    match n:
        case int():
            return "some int"
        case x if x % 2 == 0:
            return "even"
        case _:
            return "other"

print(classify_wrong(6))
some int

Unlike C switch, match never falls through, so exactly one branch runs. I verified this with a probe that collects branch hits, and only one entry ever lands.

def fallthrough_probe(v):
    out = []
    match v:
        case 1:
            out.append("one")
        case 2:
            out.append("two")
        case _:
            out.append("other")
    return out

print(fallthrough_probe(1))
['one']

The subject expression runs exactly once, which matters when it has side effects. My probe counted one call, so expensive computations stay cheap inside match.

calls = []

def expensive():
    calls.append(1)
    return 2

match expensive():
    case 1:
        print("one")
    case 2:
        print("two")

print("calls:", len(calls))
two
calls: 1

About speed, honesty first. Python match has no jump-table optimization under the hood, so pick it for readability, not for a performance story the interpreter cannot cash.

Class shapes get their own branch style too. Attribute matching on objects reads naturally once you see it run.

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

def where(p):
    match p:
        case Point(x=0, y=0):
            return "origin"
        case Point(x=0, y=y):
            return f"y-axis at {y}"
        case Point(x=x, y=y):
            return f"at {x},{y}"

print(where(Point(0, 0)))
print(where(Point(0, 9)))
print(where(Point(2, 3)))
origin
y-axis at 9
at 2,3

Which Branch to Reach For

Small chain with different logic per branch, keep if-elif, since everyone reads it instantly. Pure value lookup, take the dict, since one get call replaces the chain.

Grouped values, destructuring, or guards, take match, because the other two bend themselves out of shape expressing those. The readability-first crowd has a fair point here, so reach for match where its shape work shows, not as a default.

SituationReach for
Few branches, different logic eachIf-elif chain
Many inputs, one value eachDict lookup
Groups, shapes, or guarded valuesMatch statement
Must run on 3.9 or olderIf-elif or dict only

Frequently Asked Questions

Direct answers to the switch questions readers keep asking. Each one points back at the section that proves it.

Does Python have a switch statement?

Python has no switch keyword. Before 3.10, if-elif chains and dict lookups filled the role. Since 3.10, the match statement covers it and adds destructuring of sequences, mappings, and objects on top.

Which Python version supports match-case?

Python 3.10 introduced match-case through PEP 634. Code using it fails on 3.9 and older, so gate version-sensitive projects or keep if-elif and dict dispatch for those.

Is match-case faster than if-elif?

No jump-table optimization sits behind match, so speed is not the reason to choose it. Choose match when branches group values, destructure data, or need guards, and keep if-elif for short chains.

Why does my constant in a case clause not match?

A bare name in a case clause captures the value instead of comparing it. Use a dotted name such as Codes.NOT_FOUND, which match compares as a value.

Does Python match fall through like C switch?

Never. The first matching branch runs and the rest are skipped, with no break statement needed. Order branches from narrowest to broadest so specific cases fire first.

Pankaj Kumar
Pankaj Kumar

I have been working on Python programming for more than 12 years. At AskPython, I share my learning on Python with other fellow developers.

Articles: 256