Python

Python Switch Statement: Match-Case & Alternatives Guide

Does Python have a switch statement? Learn Python 3.10+ match-case syntax, dictionary dispatch patterns, and if-elif alternatives with code examples.

By InventiveHQ Team

Does Python Have a Switch Statement?

Yes and no. Traditional switch statements (like in C, Java, or JavaScript) weren't part of Python until version 3.10. However, Python 3.10 introduced structural pattern matching with the match statement, which provides switch-like functionality—and more.

For Python versions before 3.10, developers use if-elif chains or dictionary dispatch patterns as switch alternatives. Both approaches are still widely used and often preferred even in newer Python versions.

Understanding Switch Statements

Switch statements provide a way to execute different blocks of code based on the value of a variable. They're designed to be more readable and potentially more efficient than long chains of if-else statements. Here's how a traditional switch statement looks in C:

// Traditional C switch statement
switch(key) {
    case 'a':
        result = 1;
        break;
    case 'b':
        result = 2;
        break;
    case 'c':
        result = 3;
        break;
    default:
        result = -1;
}

Why Python Doesn't Have Switch Statements

Python's philosophy emphasizes simplicity and readability. The language designers felt that existing constructs like if-elif chains and dictionaries provide sufficient functionality without adding another keyword. Python 3.10 did introduce structural pattern matching with the match statement, but the traditional alternatives remain widely used and important to understand.

  • Simplicity: Fewer language constructs to learn

  • Flexibility: More powerful alternatives available

  • Consistency: Fits with Python's overall design philosophy

Alternative 1: If-Elif Chains

The most straightforward alternative to switch statements is using if-elif-else chains. This approach is readable and familiar to most programmers, making it an excellent choice for simple conditional logic.

# Basic if-elif chain
def process_grade(letter):
    if letter == 'A':
        return "Excellent - 90-100%"
    elif letter == 'B':
        return "Good - 80-89%"
    elif letter == 'C':
        return "Average - 70-79%"
    elif letter == 'D':
        return "Below Average - 60-69%"
    elif letter == 'F':
        return "Failing - Below 60%"
    else:
        return "Invalid grade"

# Usage example
grade = 'B'
result = process_grade(grade)
print(result)  # Output: "Good - 80-89%"

# More complex example with multiple conditions
def determine_season(month):
    if month in [12, 1, 2]:
        return "Winter"
    elif month in [3, 4, 5]:
        return "Spring"
    elif month in [6, 7, 8]:
        return "Summer"
    elif month in [9, 10, 11]:
        return "Fall"
    else:
        return "Invalid month"

Pros and Cons of If-Elif Chains

Advantages:

  • Highly readable and intuitive

  • Supports complex conditions

  • Easy to debug and modify

  • Works with any data type

Disadvantages:

  • Sequential evaluation (O(n) performance)

  • Can become verbose with many conditions

  • Later conditions execute slower

Performance Note: If-elif chains evaluate conditions sequentially from top to bottom, which means frequently used conditions should be placed first for optimal performance.

Alternative 2: Dictionary Lookup

Dictionary lookups provide the performance benefits of traditional switch statements with O(1) average-case lookup time. This approach is particularly effective for simple value mappings and function dispatching.

Simple Value Mapping

# Basic dictionary lookup
grade_mapping = {
    'A': "Excellent - 90-100%",
    'B': "Good - 80-89%",
    'C': "Average - 70-79%",
    'D': "Below Average - 60-69%",
    'F': "Failing - Below 60%"
}

# Simple lookup with default
key = 'B'
result = grade_mapping.get(key, "Invalid grade")
print(result)  # Output: "Good - 80-89%"

# HTTP status code mapping
http_status = {
    200: "OK",
    201: "Created",
    400: "Bad Request",
    401: "Unauthorized",
    404: "Not Found",
    500: "Internal Server Error"
}

status_code = 404
message = http_status.get(status_code, "Unknown Status")
print(f"Status {status_code}: {message}")  # Output: "Status 404: Not Found"

Function Dispatch Pattern

# Function dispatch using dictionary
def handle_create():
    return "Creating new resource"

def handle_read():
    return "Reading existing resource"

def handle_update():
    return "Updating resource"

def handle_delete():
    return "Deleting resource"

def handle_default():
    return "Unknown operation"

# Dictionary mapping operations to functions
operations = {
    'CREATE': handle_create,
    'READ': handle_read,
    'UPDATE': handle_update,
    'DELETE': handle_delete
}

# Function dispatch
operation = 'UPDATE'
handler = operations.get(operation, handle_default)
result = handler()
print(result)  # Output: "Updating resource"

# More advanced example with parameters
def calculate_area(shape, **kwargs):
    def circle_area():
        return 3.14159 * kwargs['radius'] ** 2

    def rectangle_area():
        return kwargs['length'] * kwargs['width']

    def triangle_area():
        return 0.5 * kwargs['base'] * kwargs['height']

    calculators = {
        'circle': circle_area,
        'rectangle': rectangle_area,
        'triangle': triangle_area
    }

    calculator = calculators.get(shape.lower())
    if calculator:
        return calculator()
    else:
        return "Unknown shape"

# Usage
area = calculate_area('circle', radius=5)
print(f"Circle area: {area}")  # Output: Circle area: 78.53975

Performance Advantage: Dictionary lookups have O(1) average-case time complexity, making them significantly faster than if-elif chains for large numbers of conditions.

Python 3.10+ Match Statements

Python 3.10 introduced structural pattern matching with the match statement, providing a more powerful alternative to traditional switch statements. This feature supports complex pattern matching beyond simple value comparisons.

# Python 3.10+ match statement
def process_http_status(status_code):
    match status_code:
        case 200:
            return "OK - Request successful"
        case 201:
            return "Created - Resource created successfully"
        case 400:
            return "Bad Request - Invalid request format"
        case 401:
            return "Unauthorized - Authentication required"
        case 404:
            return "Not Found - Resource does not exist"
        case 500:
            return "Internal Server Error - Server malfunction"
        case _:  # Default case
            return f"Unknown status code: {status_code}"

# Advanced pattern matching with guards
def categorize_number(value):
    match value:
        case x if x < 0:
            return "Negative number"
        case 0:
            return "Zero"
        case x if x > 100:
            return "Large positive number"
        case x:
            return f"Small positive number: {x}"

# Pattern matching with data structures
def process_command(command):
    match command:
        case {"action": "move", "direction": direction, "distance": distance}:
            return f"Moving {direction} for {distance} units"
        case {"action": "rotate", "angle": angle}:
            return f"Rotating by {angle} degrees"
        case {"action": "stop"}:
            return "Stopping all movement"
        case _:
            return "Unknown command"

# Usage examples
print(process_command({"action": "move", "direction": "north", "distance": 10}))
print(process_command({"action": "rotate", "angle": 90}))

The trap: a bare name in a case is not a comparison

This is the mistake nearly everyone makes on their first real match statement. A bare identifier is a capture pattern, not a comparison: it matches anything and binds that name to the subject.

HTTP_OK = 200
HTTP_NOT_FOUND = 404

def describe(status):
    match status:
        case HTTP_OK:          # does NOT mean "if status == 200"
            return "OK"
        case HTTP_NOT_FOUND:   # unreachable
            return "Not Found"
        case _:                # also unreachable
            return "Other"

Write exactly that and Python refuses to compile the file:

SyntaxError: name capture 'HTTP_OK' makes remaining patterns unreachable

That error is a genuine kindness, and it is why this bug is usually caught immediately. But the check only fires when there are later patterns to render unreachable. In three shapes it compiles cleanly and quietly does the wrong thing:

TIMEOUT = 504

# 1. Capture as the LAST case — no remaining patterns, so no SyntaxError
def classify(status):
    match status:
        case 200:
            return "OK"
        case TIMEOUT:        # matches ANY non-200 status
            return "timeout"

classify(500)   # 'timeout'  ← wrong, and no warning

# 2. A single case, with nothing after it
def only(status):
    match status:
        case TIMEOUT:
            return "timeout"

only(500)       # 'timeout'  ← wrong

# 3. A guard makes the pattern refutable, so the check is suppressed
def guarded(v):
    match v:
        case TIMEOUT if v > 100:
            return "big"
        case _:
            return "small"

There is a second surprise worth knowing: the capture binds a local variable in the enclosing function scope. It shadows the constant for the rest of that function but leaves the module-level name alone.

TIMEOUT = 504

def classify(status):
    match status:
        case 200:
            return "OK"
        case TIMEOUT:
            print(TIMEOUT)   # 500 — the subject, not 504
            return "timeout"

classify(500)
print(TIMEOUT)               # 504 — the global is untouched

Python's grammar requires a value pattern — a dotted name — to compare against a named constant. There are three correct fixes:

import http

# 1. Dotted name (value pattern) — compares, does not bind
def describe(status):
    match status:
        case http.HTTPStatus.OK:
            return "OK"
        case http.HTTPStatus.NOT_FOUND:
            return "Not Found"
        case _:
            return "Other"

# 2. Enum members are dotted, so they work directly
from enum import Enum

class State(Enum):
    IDLE = "idle"
    RUNNING = "running"

def tick(state):
    match state:
        case State.IDLE:
            return "waiting"
        case State.RUNNING:
            return "working"

# 3. Literals need no workaround at all
def describe_literal(status):
    match status:
        case 200:
            return "OK"
        case 404:
            return "Not Found"
        case _:
            return "Other"

Rule of thumb: if the thing after case is a bare lowercase word, you are capturing, not comparing. Literals, dotted names, and enum members compare. Everything else binds.

Advertisement

What match-case does that a switch cannot

Calling match "Python's switch" undersells it. A C switch compares one scalar against constants. match destructures data — it tests shape and pulls values out in the same step. These are the capabilities that actually justify reaching for it.

Sequence patterns (destructuring)

def parse_command(argv):
    match argv:
        case []:
            return "no command given"
        case ["deploy"]:
            return "deploy to default env"
        case ["deploy", env]:
            return f"deploy to {env}"
        case ["deploy", env, *flags]:
            return f"deploy to {env} with {len(flags)} flag(s)"
        case [cmd, *_]:
            return f"unknown command: {cmd}"

parse_command(["deploy", "prod", "--force", "--verbose"])
# 'deploy to prod with 2 flag(s)'

Note that a sequence pattern matches lists and tuples, but not str, bytes, or bytearray — a deliberate design choice so case [x, y]: never accidentally matches a two-character string.

OR patterns

def category(status):
    match status:
        case 200 | 201 | 204:
            return "success"
        case 301 | 302 | 307 | 308:
            return "redirect"
        case 400 | 401 | 403 | 404:
            return "client error"
        case _:
            return "other"

Subpatterns are tried left to right. If they bind names, every alternative must bind the same set of names, or Python raises a SyntaxError.

Class patterns

This is the capability with no switch equivalent at all — matching on type and attributes at once:

from dataclasses import dataclass

@dataclass
class Point:
    x: float
    y: float

@dataclass
class Circle:
    center: Point
    radius: float

def describe(shape):
    match shape:
        case Point(x=0, y=0):
            return "origin"
        case Point(x=0, y=y):
            return f"on the y-axis at {y}"
        case Point(x=x, y=0):
            return f"on the x-axis at {x}"
        case Circle(center=Point(x=0, y=0), radius=r):
            return f"circle centred on origin, radius {r}"
        case Circle():
            return "some other circle"
        case _:
            return "unknown shape"

Positional patterns like Point(0, 0) also work, but only if the class defines __match_args__. Dataclasses and NamedTuple get it for free; plain classes do not, and Point(0, 0) against a plain class raises TypeError.

Mapping patterns match on a subset

A mapping pattern requires the listed keys to be present, and ignores any extras. That makes it well suited to JSON payloads where you only care about a few fields:

def handle(event):
    match event:
        case {"type": "user.created", "id": user_id}:
            return f"provision user {user_id}"
        case {"type": "user.deleted", "id": user_id, **rest}:
            return f"deprovision user {user_id} (extra keys: {list(rest)})"
        case {"type": str(kind)}:
            return f"unhandled event type {kind}"
        case _:
            return "malformed event"

handle({"type": "user.created", "id": 42, "source": "api", "ts": 1234})
# 'provision user 42' — the extra keys do not prevent the match

Because extras are allowed, order your cases from most specific to least specific. A broad case {"type": _}: placed early will shadow everything below it.

Guards for conditions patterns cannot express

def route(request):
    match request:
        case {"path": path} if path.startswith("/admin"):
            return "admin handler"
        case {"path": path, "method": "POST"} if len(path) > 200:
            return "reject: path too long"
        case {"method": "GET"}:
            return "read handler"
        case _:
            return "default handler"

The guard runs only after the pattern matches and its names are bound — so you can reference captured names inside the guard. If the guard is false, matching continues with the next case.

Choosing the Right Approach

Selecting the best alternative depends on your specific use case, performance requirements, and Python version. Here's a decision matrix to help you choose:

ScenarioRecommended approachReason
2–5 simple conditionsIf-elif chainMost readable; no machinery to justify
Many value-to-value mappingsDictionary lookupO(1) average lookup, concise, data-driven
Dispatching to functions by keyDictionary of callablesClean separation; handlers can be registered at runtime
Matching on the shape of datamatch (3.10+)Destructures lists, dicts, and objects in one step
Matching on type plus attributesmatch with class patternsNo idiomatic alternative — if-elif needs nested isinstance
Parsing commands, ASTs, JSON eventsmatch (3.10+)Exactly what structural pattern matching was designed for
Complex boolean conditions on several variablesIf-elif chainmatch matches one subject; if-elif has no such limit
Must support Python 3.9 or olderDict dispatch or if-elifmatch is a syntax error before 3.10 — not a runtime check

When each is actually the right call

The three approaches are not interchangeable, and "which is fastest" is usually the wrong question — dispatch is rarely the bottleneck.

  • Reach for if-elif when your conditions involve more than one variable, ranges, or arbitrary boolean logic. match matches a single subject; forcing multi-variable logic through guards reads worse than the if-elif it replaced.
  • Reach for dict dispatch when the mapping is data: it can be built from config, extended by plugins, iterated for a help listing, or unit-tested as a table. That flexibility, not the O(1) lookup, is the real reason to choose it.
  • Reach for match when you are inspecting the structure of incoming data — parsing argv, walking an AST, handling webhook payloads, matching on dataclass variants. If your cases are all bare literals, match buys you little over a dict.

A note on the performance framing: CPython compiles a match over literal patterns efficiently, but pattern complexity drives the cost, and a dict lookup stays O(1) regardless. For the handful of cases most code has, all three are far too fast to matter. Choose on clarity.

Best Practices Summary

  • Readability first: Choose the approach that makes your code most understandable

  • Consider performance: Use dictionary lookups for performance-critical code with many conditions

  • Plan for maintainability: Dictionary approaches are often easier to modify and extend

  • Use appropriate data structures: Leverage Python's built-in types for cleaner solutions

  • Consider future requirements: Choose approaches that will scale with your needs

# Example combining multiple approaches
class TaskProcessor:
    def __init__(self):
        # Dictionary for simple mappings
        self.priority_levels = {
            1: "Low",
            2: "Medium",
            3: "High",
            4: "Critical"
        }

        # Dictionary for function dispatch
        self.handlers = {
            'email': self._send_email,
            'sms': self._send_sms,
            'push': self._send_push_notification
        }

    def get_priority_name(self, level):
        return self.priority_levels.get(level, "Unknown")

    def process_notification(self, method, message):
        handler = self.handlers.get(method)
        if handler:
            return handler(message)
        else:
            return f"Unsupported notification method: {method}"

    def _send_email(self, message):
        return f"Email sent: {message}"

    def _send_sms(self, message):
        return f"SMS sent: {message}"

    def _send_push_notification(self, message):
        return f"Push notification sent: {message}"

Python Switch FAQ

When should I use match-case vs dictionary lookup?

Use match-case when you need pattern matching with data structures, type checking, or guard conditions. Use dictionary lookup for simple value-to-value or value-to-function mappings where O(1) performance matters.

Is match-case the same as a switch statement?

Not exactly. Python's match-case is more powerful—it supports structural pattern matching, which can match and destructure complex data types like tuples, lists, and dictionaries. Traditional switch statements only compare values.

Can I use match-case in Python 3.9?

No, match-case requires Python 3.10 or later. For earlier versions, use if-elif chains or dictionary dispatch patterns.

Which approach is fastest?

Dictionary lookups are O(1) on average, making them fastest for large numbers of cases. If-elif chains are O(n) in worst case. Match-case performance varies based on pattern complexity.

Frequently Asked Questions

Does Python have a switch statement?

Python 3.10+ has match-case, which provides switch-like functionality with pattern matching. Before Python 3.10, Python did not have a traditional switch statement. Instead, developers use if-elif chains or dictionary dispatch patterns as switch alternatives. The match-case statement is more powerful than traditional switch, supporting structural pattern matching. Learn more in our Python error handling guide.

How do I write a switch case in Python?

In Python 3.10+, use match-case: match variable: case value1: do_something() case value2: do_other() case _: default_action(). For older Python versions, use dictionary lookup: actions = {'a': func1, 'b': func2}; actions.get(key, default)() or if-elif chains for simple conditions.

What is the Python equivalent of switch?

Python has three switch equivalents: 1) match-case (Python 3.10+) - most similar to switch with pattern matching, 2) Dictionary dispatch - maps values to functions with O(1) lookup, 3) if-elif chains - most readable for simple conditions. Dictionary dispatch is fastest for many cases; match-case is most flexible.

Why doesn't Python have switch?

Python historically avoided switch statements because the language designers felt if-elif chains and dictionaries provided sufficient functionality without adding complexity. Python emphasizes readability and simplicity over having multiple ways to do the same thing. Python 3.10 added match-case because structural pattern matching offered capabilities beyond simple value comparison.

What is the difference between match-case and switch?

Python's match-case is more powerful than traditional switch. While switch only compares values, match-case supports structural pattern matching - it can destructure objects, match types, use guard conditions, and capture values. For example, match-case can match dictionary structures like case {'action': 'move', 'x': x, 'y': y} to extract x and y values.

Why does my match-case always hit the first case?

Because a bare variable name in a case is a capture pattern, not a comparison. case HTTP_OK: does not test whether the subject equals HTTP_OK — it matches anything and binds HTTP_OK to the subject. When other cases follow, Python refuses to compile it and raises SyntaxError: name capture 'HTTP_OK' makes remaining patterns unreachable. The dangerous version is a bare name in the final case, in a lone case, or in a case with a guard: those compile fine and silently match everything. To compare against a named constant, use a dotted value pattern such as case http.HTTPStatus.OK: or an enum member like case State.IDLE:.

Which Python switch alternative is fastest?

Dictionary dispatch is fastest with O(1) average lookup time, ideal for large numbers of cases. If-elif chains have O(n) time complexity - earlier conditions execute faster. Match-case performance varies by pattern complexity. For 2-5 simple conditions, if-elif is fine; for many conditions or performance-critical code, use dictionary dispatch. See our Python scripting basics guide for more patterns.

python switch statementpython switch casepython case statementpython match casepython switchpython if else