DEV Community

websilvercraft
websilvercraft

Posted on Originally published at pythontutorial.org

Python's match-case is not a switch statement (it's better)

When match-case landed in Python 3.10, half the internet said "finally, Python has switch." That framing undersells it badly. If you use match as a switch, you'll conclude it's pointless — if/elif did that already.

What match actually does: it checks the shape of your data and destructures it in the same step. That's a different tool, and for certain code — API payloads, command parsers, state machines — it's a dramatically better one.

The switch part (30 seconds)

Yes, it does the boring thing:

match command:
    case "quit":
        save_and_exit()
    case "help":
        show_help()
    case _:                    # wildcard: matches anything
        print(f"Unknown command: {command}")
Enter fullscreen mode Exit fullscreen mode

Fine. if/elif is just as good here. Moving on.

The real feature: matching structure

Say you're handling webhook events. The if version:

def handle(event):
    if isinstance(event, dict) and event.get("type") == "user.created":
        payload = event.get("payload")
        if payload is not None:
            create_user(payload)
    elif isinstance(event, dict) and event.get("type") == "invoice.failed":
        ...
Enter fullscreen mode Exit fullscreen mode

The match version says the same thing the way you'd describe it out loud:

def handle(event):
    match event:
        case {"type": "user.created", "payload": payload}:
            create_user(payload)
        case {"type": "invoice.failed", "invoice_id": inv_id}:
            alert_finance(inv_id)
        case {"type": str(other_type)}:
            log.warning("unhandled event type %s", other_type)
        case _:
            raise ValueError(f"malformed event: {event!r}")
Enter fullscreen mode Exit fullscreen mode

Each case simultaneously:

  1. checks the value is a dict,
  2. checks the keys and any literal values,
  3. binds the parts you care about to names (payload, inv_id).

The type check, the key check, and the unpacking collapse into one readable line. Extra keys in the dict are ignored — exactly what you want for API payloads that grow fields over time.

Sequences, classes, and guards

Sequences destructure like unpacking, with literals mixed in:

match args:
    case []:
        show_usage()
    case ["--version"]:
        print(VERSION)
    case ["run", script, *rest]:
        run(script, extra_args=rest)
Enter fullscreen mode Exit fullscreen mode

Class patterns match attributes — clean over dataclasses:

@dataclass
class Click:
    x: int
    y: int
    button: str = "left"

match event:
    case Click(x=0, y=0):
        print("origin clicked")
    case Click(x=x, y=y, button="right"):
        open_context_menu(x, y)
    case KeyPress(key="q"):
        quit()
Enter fullscreen mode Exit fullscreen mode

Guards bolt a boolean condition onto a structural match:

match order:
    case {"items": items, "total": total} if total > 10_000:
        require_manual_review(order)
    case {"items": []}:
        raise EmptyOrderError
Enter fullscreen mode Exit fullscreen mode

Alternatives with | share one body:

case "y" | "yes" | "ok":
    proceed()
Enter fullscreen mode Exit fullscreen mode

The classic gotcha

A bare name in a pattern is a capture, not a comparison:

RED = "red"

match color:
    case RED:          # does NOT compare against "red" —
        stop()         # it matches ANYTHING and rebinds RED to it
    case "blue":       # Python even raises SyntaxError here:
        go()           # "name capture 'RED' makes remaining patterns unreachable"
Enter fullscreen mode Exit fullscreen mode

To compare against a constant, use a dotted name — that's the rule the language chose:

class Color:
    RED = "red"
    BLUE = "blue"

match color:
    case Color.RED:      # dotted = lookup and compare ✓
        stop()
    case Color.BLUE:
        go()
Enter fullscreen mode Exit fullscreen mode

Ruff and recent Python versions warn about suspicious always-true capture patterns, but knowing the rule beats trusting the linter.

When to still use if/elif

  • Comparing one value against a few literals with no destructuring — if is simpler and works on every Python version.
  • Conditions on different variables per branch (if x > 3 ... elif name == "bob") — that's not what match is for.
  • Your code must run on Python < 3.10.

The sweet spot for match is exactly: "the shape of this data determines what happens next." Parsers, event handlers, recursive tree walks, REPL commands. In that territory it deletes entire pyramids of isinstance/.get() boilerplate.

Top comments (0)