Python Switch Case: match-case Statement Explained
Updated on
You are writing a command processor in Python. The function takes a string like "quit", "save", "load", or "help" and needs to run different logic for each command. In JavaScript or C you would use a switch statement. In Python, you write a chain of if/elif/else blocks — five lines for two commands, fifteen for six, thirty for twelve. The function grows into a wall of comparisons that is tedious to read, easy to misorder, and painful to extend.
This was a real limitation of Python for over 30 years. Every other major language had a switch or case construct, but Python relied entirely on if/elif chains and dictionary dispatch. Guido van Rossum rejected switch-case proposals multiple times, arguing that if/elif was sufficient.
That changed in Python 3.10 with PEP 634 (opens in a new tab), which introduced the match-case statement — Python's answer to switch-case, but far more powerful. Instead of just matching values, it can destructure objects, match patterns, bind variables, and apply conditions. It is structural pattern matching, not just a switch replacement.
Quick Syntax: match-case in 30 Seconds
def handle_command(command):
match command:
case "quit":
print("Exiting program")
case "save":
print("Saving file")
case "help":
print("Showing help")
case _:
print(f"Unknown command: {command}")
handle_command("save") # Saving file
handle_command("hello") # Unknown command: helloThe match keyword evaluates the subject expression once. Each case defines a pattern. Python tests patterns top to bottom and executes the first match. The wildcard _ is the default case — it matches anything.
match-case vs switch-case: Key Differences
If you are coming from C, Java, JavaScript, or Go, here are the critical differences:
| Feature | Python match-case | C/Java switch-case |
|---|---|---|
| Fall-through | No (each case is independent) | Yes (requires break) |
| Pattern types | Literals, sequences, mappings, classes, OR, guards | Mostly literals only |
| Destructuring | Built-in (bind variables from matched structure) | Not supported |
| Variable binding | Captures values from matched pattern | Not supported |
| Default case | case _: | default: |
| Expression/statement | Statement (no return value) | Statement |
| Minimum Python | 3.10+ | N/A |
The most important difference: no fall-through. Each case block is isolated. You never need a break statement, and you never accidentally execute the next case.
Pattern Types: What match-case Can Do
Python's match-case supports seven pattern types. This is what makes it fundamentally more powerful than a traditional switch statement.
1. Literal Patterns
Match exact values — strings, numbers, booleans, None:
def classify_status(code):
match code:
case 200:
return "OK"
case 301:
return "Moved Permanently"
case 404:
return "Not Found"
case 500:
return "Internal Server Error"
case _:
return f"Unknown status: {code}"
print(classify_status(404)) # Not Found2. OR Patterns (Multiple Values)
Use the | operator to match several values in one case:
def classify_http_status(code):
match code:
case 200 | 201 | 204:
return "Success"
case 301 | 302 | 307 | 308:
return "Redirect"
case 400 | 401 | 403 | 404:
return "Client Error"
case 500 | 502 | 503:
return "Server Error"
case _:
return "Unknown"This replaces the common if code in (200, 201, 204): pattern.
3. Capture Patterns (Variable Binding)
A bare name in a case captures the matched value into a variable:
def process_value(data):
match data:
case 0:
print("Zero")
case value:
print(f"Got non-zero value: {value}")
process_value(42) # Got non-zero value: 42Important trap: A bare name always captures — it does not compare against an existing variable. This is the most common mistake:
# WRONG — this does NOT compare against expected_code
expected_code = 200
match response_code:
case expected_code: # This captures response_code into expected_code!
print("Match")
# RIGHT — use a guard or literal
match response_code:
case code if code == expected_code:
print("Match")To match against constants, use dotted names (like http.HTTPStatus.OK) or guard clauses.
4. Sequence Patterns (Lists and Tuples)
Destructure lists and tuples directly in the pattern:
def process_point(point):
match point:
case (0, 0):
return "Origin"
case (x, 0):
return f"On x-axis at {x}"
case (0, y):
return f"On y-axis at {y}"
case (x, y):
return f"Point at ({x}, {y})"
print(process_point((3, 0))) # On x-axis at 3
print(process_point((2, 5))) # Point at (2, 5)You can use *rest to capture remaining elements:
def first_and_rest(items):
match items:
case []:
return "Empty list"
case [single]:
return f"Single item: {single}"
case [first, *rest]:
return f"First: {first}, remaining: {rest}"
print(first_and_rest([1, 2, 3, 4])) # First: 1, remaining: [2, 3, 4]5. Mapping Patterns (Dictionaries)
Match against dictionary keys and destructure values:
def handle_event(event):
match event:
case {"type": "click", "x": x, "y": y}:
print(f"Click at ({x}, {y})")
case {"type": "keypress", "key": key}:
print(f"Key pressed: {key}")
case {"type": "scroll", "direction": direction}:
print(f"Scroll {direction}")
case _:
print("Unknown event")
handle_event({"type": "click", "x": 100, "y": 200})
# Click at (100, 200)
handle_event({"type": "keypress", "key": "Enter", "modifiers": ["Ctrl"]})
# Key pressed: Enter (extra keys are ignored)Mapping patterns match if the specified keys are present — extra keys in the dictionary are silently ignored. This makes them ideal for processing JSON data or API responses.
6. Class Patterns
Match against class instances and extract attributes:
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
@dataclass
class Circle:
center: Point
radius: float
@dataclass
class Rectangle:
corner: Point
width: float
height: float
def describe_shape(shape):
match shape:
case Circle(center=Point(x=0, y=0), radius=r):
return f"Circle at origin with radius {r}"
case Circle(center=center, radius=r):
return f"Circle at ({center.x}, {center.y}) with radius {r}"
case Rectangle(width=w, height=h) if w == h:
return f"Square with side {w}"
case Rectangle(width=w, height=h):
return f"Rectangle {w}x{h}"
print(describe_shape(Circle(Point(0, 0), 5)))
# Circle at origin with radius 5
print(describe_shape(Rectangle(Point(1, 1), 4, 4)))
# Square with side 47. Guard Clauses
Add if conditions to any pattern for additional filtering:
def classify_number(n):
match n:
case n if n < 0:
return "Negative"
case 0:
return "Zero"
case n if n % 2 == 0:
return "Positive even"
case _:
return "Positive odd"
print(classify_number(-5)) # Negative
print(classify_number(4)) # Positive even
print(classify_number(7)) # Positive oddGuards let you add arbitrary conditions beyond what patterns can express. The pattern is matched first, then the guard condition is evaluated.
Real-World Example: JSON API Response Parser
One of the most practical uses of match-case is parsing API responses or JSON data:
import json
def process_api_response(response: dict) -> str:
match response:
case {"status": "success", "data": {"users": [*users]}}:
return f"Found {len(users)} users"
case {"status": "success", "data": {"user": {"name": name, "email": email}}}:
return f"User: {name} ({email})"
case {"status": "error", "code": 401}:
return "Authentication required"
case {"status": "error", "code": code, "message": msg}:
return f"Error {code}: {msg}"
case {"status": "error"}:
return "Unknown error"
case _:
return "Unrecognized response format"
# Test with different responses
print(process_api_response({
"status": "success",
"data": {"users": ["Alice", "Bob", "Charlie"]}
}))
# Found 3 users
print(process_api_response({
"status": "error",
"code": 429,
"message": "Rate limit exceeded"
}))
# Error 429: Rate limit exceededThis replaces deeply nested if/elif chains with isinstance() checks and dictionary .get() calls. The match-case version states the expected structure directly.
Real-World Example: Command-Line Argument Parser
import sys
def parse_args(args: list[str]):
match args:
case [program]:
print(f"Usage: {program} <command> [options]")
case [_, "init", project_name]:
print(f"Initializing project: {project_name}")
case [_, "init"]:
print("Error: project name required")
case [_, "build", "--release"]:
print("Building in release mode")
case [_, "build"]:
print("Building in debug mode")
case [_, "test", *test_files] if test_files:
print(f"Running tests: {', '.join(test_files)}")
case [_, "test"]:
print("Running all tests")
case [_, unknown, *_]:
print(f"Unknown command: {unknown}")
parse_args(["app", "init", "my-project"]) # Initializing project: my-project
parse_args(["app", "test", "a.py", "b.py"]) # Running tests: a.py, b.py
parse_args(["app", "build", "--release"]) # Building in release modePre-3.10 Alternatives: Switch-Case Without match-case
If you are using Python 3.9 or earlier, or if match-case is overkill for simple value matching, here are the established alternatives.
if/elif/else Chains
The most straightforward approach:
def handle_command(command):
if command == "quit":
print("Exiting program")
elif command == "save":
print("Saving file")
elif command == "load":
print("Loading file")
elif command == "help":
print("Showing help")
else:
print(f"Unknown command: {command}")When to use: When you have fewer than 5-6 cases and the logic is simple.
Dictionary Dispatch
Map values to functions using a dictionary:
def cmd_quit():
print("Exiting program")
def cmd_save():
print("Saving file")
def cmd_help():
print("Showing help")
commands = {
"quit": cmd_quit,
"save": cmd_save,
"help": cmd_help,
}
def handle_command(command):
action = commands.get(command)
if action:
action()
else:
print(f"Unknown command: {command}")When to use: When mapping values to simple actions, especially with many cases. O(1) lookup vs O(n) for if/elif.
Dictionary Dispatch with Arguments
def calculate(operation, a, b):
ops = {
"+": lambda a, b: a + b,
"-": lambda a, b: a - b,
"*": lambda a, b: a * b,
"/": lambda a, b: a / b if b != 0 else float("inf"),
}
func = ops.get(operation)
if func is None:
raise ValueError(f"Unknown operation: {operation}")
return func(a, b)
print(calculate("+", 10, 3)) # 13
print(calculate("/", 10, 0)) # infEnum-Based Dispatch
For typed, self-documenting code:
from enum import Enum
class Direction(Enum):
NORTH = "north"
SOUTH = "south"
EAST = "east"
WEST = "west"
def move(direction: Direction, x: int, y: int) -> tuple[int, int]:
offsets = {
Direction.NORTH: (0, 1),
Direction.SOUTH: (0, -1),
Direction.EAST: (1, 0),
Direction.WEST: (-1, 0),
}
dx, dy = offsets[direction]
return (x + dx, y + dy)
print(move(Direction.NORTH, 0, 0)) # (0, 1)Comparison: Which Approach to Use
| Approach | Python Version | Best For | Complexity | Performance |
|---|---|---|---|---|
match-case | 3.10+ | Complex patterns, destructuring, type matching | High expressiveness | Comparable to if/elif |
if/elif/else | All | Simple value comparisons, < 6 cases | Simple | O(n) linear scan |
| Dict dispatch | All | Many cases, value → function mapping | Medium | O(1) lookup |
| Dict + lambda | All | Many cases, value → expression mapping | Medium | O(1) lookup |
| Enum dispatch | All | Typed constants, exhaustive matching | Medium | O(1) lookup |
Rule of thumb:
- Use match-case when you need destructuring, type matching, or complex patterns
- Use if/elif for simple comparisons with few branches
- Use dict dispatch for many simple value-to-action mappings
- Use enum dispatch when the set of values is fixed and typed
Common Patterns and Recipes
Type-Based Dispatch
def serialize(value):
match value:
case bool(): # Must come before int — bool is a subclass of int
return "true" if value else "false"
case int() | float():
return str(value)
case str():
return f'"{value}"'
case list():
items = ", ".join(serialize(v) for v in value)
return f"[{items}]"
case dict():
pairs = ", ".join(
f'{serialize(k)}: {serialize(v)}' for k, v in value.items()
)
return "{" + pairs + "}"
case None:
return "null"
case _:
raise TypeError(f"Cannot serialize {type(value)}")
print(serialize({"name": "Alice", "scores": [95, 87, 92], "active": True}))
# {"name": "Alice", "scores": [95, 87, 92], "active": true}Important: Put bool() before int() because True and False are instances of int in Python.
Nested Pattern Matching
def evaluate(expr):
"""Simple math expression evaluator."""
match expr:
case int(n) | float(n):
return n
case ("+", left, right):
return evaluate(left) + evaluate(right)
case ("-", left, right):
return evaluate(left) - evaluate(right)
case ("*", left, right):
return evaluate(left) * evaluate(right)
case ("/", left, right):
divisor = evaluate(right)
if divisor == 0:
raise ZeroDivisionError("Division by zero")
return evaluate(left) / divisor
case ("neg", operand):
return -evaluate(operand)
case _:
raise ValueError(f"Invalid expression: {expr}")
# (3 + 4) * 2
result = evaluate(("*", ("+", 3, 4), 2))
print(result) # 14State Machine with match-case
def tokenize(text):
"""Simple tokenizer using match-case as a state machine."""
tokens = []
i = 0
while i < len(text):
match text[i]:
case ' ' | '\t' | '\n':
i += 1 # Skip whitespace
case '+' | '-' | '*' | '/':
tokens.append(("OP", text[i]))
i += 1
case '(' | ')':
tokens.append(("PAREN", text[i]))
i += 1
case c if c.isdigit():
j = i
while j < len(text) and text[j].isdigit():
j += 1
tokens.append(("NUM", int(text[i:j])))
i = j
case c if c.isalpha():
j = i
while j < len(text) and text[j].isalnum():
j += 1
tokens.append(("ID", text[i:j]))
i = j
case c:
raise SyntaxError(f"Unexpected character: {c}")
return tokens
print(tokenize("x + 42 * (y - 3)"))
# [('ID', 'x'), ('OP', '+'), ('NUM', 42), ('OP', '*'), ('PAREN', '('),
# ('ID', 'y'), ('OP', '-'), ('NUM', 3), ('PAREN', ')')]Performance: match-case vs if/elif vs dict
For simple value matching, all three approaches are fast enough that the difference does not matter in practice. Here are benchmark results for matching against 10 string values:
import timeit
commands = ["cmd_0", "cmd_1", "cmd_2", "cmd_3", "cmd_4",
"cmd_5", "cmd_6", "cmd_7", "cmd_8", "cmd_9"]
# if/elif approach
def if_elif_dispatch(cmd):
if cmd == "cmd_0": return 0
elif cmd == "cmd_1": return 1
elif cmd == "cmd_2": return 2
elif cmd == "cmd_3": return 3
elif cmd == "cmd_4": return 4
elif cmd == "cmd_5": return 5
elif cmd == "cmd_6": return 6
elif cmd == "cmd_7": return 7
elif cmd == "cmd_8": return 8
elif cmd == "cmd_9": return 9
else: return -1
# dict dispatch approach
dispatch_dict = {f"cmd_{i}": i for i in range(10)}
def dict_dispatch(cmd):
return dispatch_dict.get(cmd, -1)
# match-case approach
def match_dispatch(cmd):
match cmd:
case "cmd_0": return 0
case "cmd_1": return 1
case "cmd_2": return 2
case "cmd_3": return 3
case "cmd_4": return 4
case "cmd_5": return 5
case "cmd_6": return 6
case "cmd_7": return 7
case "cmd_8": return 8
case "cmd_9": return 9
case _: return -1| Approach | Best case (first match) | Worst case (last match) | Miss (no match) |
|---|---|---|---|
| if/elif | ~80ns | ~400ns | ~420ns |
| dict dispatch | ~60ns | ~60ns | ~65ns |
| match-case | ~90ns | ~450ns | ~470ns |
Key takeaway: Dictionary dispatch has O(1) constant time regardless of position. Both if/elif and match-case are O(n) linear scan. For fewer than ~20 cases, the difference is negligible. Choose based on readability, not performance.
Common Mistakes and How to Avoid Them
Mistake 1: Using match-case Before Python 3.10
# This causes SyntaxError in Python 3.9 and earlier
match command: # SyntaxError: invalid syntax
case "quit":
passCheck your Python version: python --version. If you are on 3.9 or earlier, use if/elif or dict dispatch.
Mistake 2: Comparing Against Variables
# WRONG — captures, does NOT compare
STATUS_OK = 200
match response_code:
case STATUS_OK: # This always matches and overwrites STATUS_OK!
print("OK")
# RIGHT — use dotted name
class Status:
OK = 200
NOT_FOUND = 404
match response_code:
case Status.OK:
print("OK")
case Status.NOT_FOUND:
print("Not Found")
# RIGHT — use a guard
match response_code:
case code if code == STATUS_OK:
print("OK")Mistake 3: Forgetting the Wildcard Default
# DANGEROUS — unmatched values silently do nothing
match command:
case "save":
save_file()
case "load":
load_file()
# No default — "delete" silently falls through with no action
# SAFE — always include a default case
match command:
case "save":
save_file()
case "load":
load_file()
case _:
raise ValueError(f"Unknown command: {command}")Mistake 4: Wrong Order With Overlapping Patterns
# WRONG — the first case catches everything
match value:
case x: # Captures ANY value — always matches!
print(f"Got: {x}")
case 42: # Never reached
print("The answer")
# RIGHT — specific patterns first, general last
match value:
case 42:
print("The answer")
case x:
print(f"Got: {x}")Using match-case in Data Science
If you work with data processing pipelines, match-case is useful for handling different data formats and cleaning operations:
import csv
from pathlib import Path
def load_data(source):
"""Load data from different source types."""
match source:
case str() as path if path.endswith(".csv"):
with open(path) as f:
return list(csv.DictReader(f))
case str() as path if path.endswith(".json"):
import json
with open(path) as f:
return json.load(f)
case list() as records:
return records
case dict() as single_record:
return [single_record]
case _:
raise TypeError(f"Unsupported data source: {type(source)}")For interactive data exploration, tools like PyGWalker (opens in a new tab) let you visualize DataFrames directly without writing plotting code — useful when you want to quickly inspect the data your processing pipeline produces.
If you are building data processing scripts that use match-case for complex branching, RunCell (opens in a new tab) provides an AI-powered Jupyter environment where you can test pattern matching interactively with real data samples.
match-case With Dataclasses and Named Tuples
Python's match-case works especially well with dataclasses and named tuples:
from dataclasses import dataclass
from typing import Optional
@dataclass
class LogEntry:
level: str
message: str
error: Optional[Exception] = None
def handle_log(entry: LogEntry):
match entry:
case LogEntry(level="CRITICAL", error=err) if err is not None:
send_alert(f"CRITICAL with error: {err}")
restart_service()
case LogEntry(level="CRITICAL", message=msg):
send_alert(f"CRITICAL: {msg}")
case LogEntry(level="ERROR", error=err) if err is not None:
log_to_file(f"ERROR: {err}")
case LogEntry(level="WARNING" | "ERROR", message=msg):
log_to_file(msg)
case LogEntry(level="INFO" | "DEBUG"):
pass # Ignore low-priority logsFrequently Asked Questions
Does Python have a switch statement?
Python 3.10+ has the match-case statement, which is Python's version of switch-case. It goes beyond traditional switch with structural pattern matching — you can destructure objects, match patterns, and bind variables. For Python 3.9 and earlier, use if/elif/else chains or dictionary dispatch as alternatives.
What is the difference between match-case and switch-case?
Python's match-case has no fall-through (no break needed), supports pattern matching (sequences, mappings, classes), can destructure and bind variables from matched patterns, and supports guard clauses with if conditions. Traditional switch-case in C/Java only matches literal values.
Is match-case faster than if/elif in Python?
For simple value matching, they perform similarly — both scan linearly. Dictionary dispatch is faster (O(1)) for many cases. The performance difference is negligible for fewer than 20 cases. Choose based on readability and pattern complexity, not performance.
Can I use match-case with Python 3.9?
No. The match-case statement requires Python 3.10 or later. For earlier versions, use if/elif/else chains or dictionary dispatch.
Why does my match-case variable comparison not work?
A bare name like case x: captures any value into x — it does not compare against an existing variable. To compare against a variable, use a guard clause (case val if val == x:) or dotted names (case MyClass.CONSTANT:).
What is the underscore _ in match-case?
The _ wildcard pattern matches any value without binding it to a variable. It is the default case — like default: in switch statements. Always place it last.
Related Guides
- Python Try Except: Error Handling Guide
- Python Type Hints: Complete Tutorial
- Python Decorators Explained
- Python Collections Module Guide
- Python Enumerate Function
- Python Assert Statement