AlgoMaster Logo

match-case (Python 3.10+)

Medium Priority23 min readUpdated June 6, 2026
Listen to this chapter
Unlock Audio

Python 3.10 added match and case statements, a feature called structural pattern matching. It looks like a switch statement from other languages, but it does much more: it can decompose data structures, bind names to inner values, and dispatch on the shape of an object in a single expression. This lesson covers every pattern type with E-Commerce examples and points out the pitfalls that catch newcomers.

Why Structural Pattern Matching Exists

If you've written enough if/elif chains, you've felt the pain. Imagine you're handling order status updates, and the shape of each update varies. Sometimes it's a simple status string, sometimes a dict with extra tracking info, sometimes a class instance with several fields. A traditional elif chain looks like this:

The code works, but the logic is buried under repeated isinstance checks, key tests, and indexing. You're doing two jobs at once: figuring out the shape of the data, and pulling out the values you actually care about. match/case splits those jobs apart so the shape lives in the pattern and the values fall out automatically.

The same logic with match:

Each case reads like "if the event looks like this shape, run this branch and let me use the inner values directly". That second job, pulling out the values, is what makes match more than a switch statement.

Here's the high-level flow of how Python evaluates a match block:

Python tries each pattern in order, top to bottom. The first one that matches runs, and the rest are skipped. If none match, the match block does nothing (no error, no warning). Most lessons cover the basic syntax first, so let's do that.

Basic Syntax

The structure has two keywords:

match subject: introduces the value you want to inspect. Each case pattern: checks whether the subject fits a pattern, and runs its body if it does. The _ (underscore) is the wildcard pattern: it matches anything and is the standard "default" branch.

A small order status example:

A few rules worth pinning down right away:

  • match and case are soft keywords. You can still name a variable match outside of a match statement, though doing so is a bad idea for readability.
  • There is no implicit fall-through to the next case. Each case body is independent; once one runs, the match block exits.
  • The wildcard _ matches anything but does not bind a name. You can't reference _ in the body and expect it to hold the subject (use a capture pattern for that).
  • A match with no matching case does nothing. There's no MatchError if every pattern misses.

Literal Patterns

The simplest case: matching against a constant value. Literal patterns compare the subject with == (with one exception for None, True, and False, which use is).

Literal patterns can be strings, numbers, booleans, or None:

Capture Patterns and the Critical Pitfall

A capture pattern is just a bare name. It always matches, and it binds the subject to that name inside the case body.

case name: looks like it's comparing the subject with a variable called name. It isn't. It's binding the subject to a brand new local variable called name, the same way name = customer_name would.

This is where almost every beginner gets caught. Watch what happens when you try to compare against a variable:

What's wrong with this code?

Every call returns "On the way" because case SHIPPED: is interpreted as a capture pattern, not a comparison. It binds whatever the subject is to a local variable called SHIPPED (yes, even though SHIPPED already exists as a module-level constant), and the case always matches. The second and third case clauses are unreachable.

Python actually warns you about this:

You'll see the error at import or run time. There are three ways to fix it:

Fix 1: Use the literal value directly.

Fix 2: Use a dotted name. A name with a dot in it (like module.CONST or EnumClass.MEMBER) is treated as a value pattern and compared with ==. This is the standard idiom for enum members.

OrderStatus.SHIPPED has a dot, so Python looks it up and compares the subject against the resolved value. No accidental binding.

Fix 3: Use a guard. A case clause can have an if condition. case s if s == SHIPPED: works, though for plain constants the dotted-name fix is cleaner.

The rule to memorize: a bare name in a pattern is always a capture, never a comparison. If you want to compare against a constant, use a literal or a dotted name.

OR Patterns

You can match several patterns in the same case using | (the pipe character):

The OR pattern matches if any of the sub-patterns match. Read "delivered" | "cancelled" | "refunded" as "any of these three values".

You can mix literal and capture patterns in an OR, but every sub-pattern must bind the same set of names. This works:

OR patterns also pair nicely with the wildcard inside them, which is handy when you want "this specific value, or anything else, but treat it the same":

Sequence Patterns

match can take apart lists and tuples. A sequence pattern uses [...] (or (...)) and matches by length and element-wise position.

Each case pattern matches a list of the exact length. Inside the pattern, each position is itself a sub-pattern. The bare names (item, first, second) are capture patterns that bind the element at that position.

For variable-length matches, use the star pattern *name to collect "the rest" into a list:

[first, *rest] reads as "at least one element; bind the first to first and the remaining elements (zero or more) to rest as a list". *rest always binds to a list, even if it captures zero items.

You can put * in the middle too, which is useful when you want the first and last items:

*_ is the same as *name but throws away the captured items. Use it when you only care about the positions on either side.

One subtle behavior: sequence patterns don't match strings, even though strings are sequences. This is on purpose, because matching "abc" against [first, *rest] would feel surprising. They match lists, tuples, and most other sequence types, but not str and bytes.

The tuple matches as a sequence. The string does not.

Mapping Patterns

Dictionaries get their own pattern syntax, written with {...}. A mapping pattern matches if the subject is a dictionary that contains at least the listed keys (extra keys are allowed and ignored).

A few things to notice:

  • The keys in the pattern ("status", "tracking") are literal patterns. They must match exactly.
  • The values can be literals ("shipped", "delivered") or capture patterns (tracking, status).
  • Extra keys in the subject are fine. The pattern only requires the keys it names.
  • An empty dict matches case {}:, which is "any mapping", because every dict contains "at least" the zero keys listed.

Order matters in your case list: more specific patterns must come before more general ones. {"status": "shipped", "tracking": tracking} must appear before {"status": status}, otherwise the more general pattern would catch every shipped event first and the specific one would never run.

To capture the leftover keys (everything you didn't name), use **rest:

**extras binds a dict of the keys you didn't pattern-match. The double-underscore wildcard form **_ is not allowed; if you don't want the extras, just leave **rest out entirely.

Class Patterns

match can also dispatch on the type of an object and pull out its attributes in the same step. This is the class pattern, written as ClassName(attr=pattern, ...).

You'll see class patterns most often with dataclass-style objects, but a regular class with attributes works the same way. Here's a minimal class for an order:

The pattern Order(status="shipped", total=total) does three things at once:

  1. Checks that the subject is an instance of Order (or a subclass).
  2. Checks that the status attribute equals "shipped".
  3. Binds total to a new local variable holding order.status's sibling, order.total.

If any of those checks fail, the pattern moves on to the next case. There's no separate isinstance call, no getattr, no nested if. The pattern carries all three concerns.

You can match on multiple classes for the same shape using OR:

Both sides of the | must bind the same names (here, amount), which they do.

Classes can also support positional class patterns by defining a __match_args__ tuple, but the keyword form (status=..., total=...) is the form to learn first. It reads clearly, doesn't depend on argument order, and works with any class out of the box.

Guards

A pattern can have an if clause attached, called a guard. The case matches only when the pattern fits and the guard is true.

The pattern Order(total=total) matches every Order. The guard if total > 100 is what narrows it down. Inside a guard, you can reference any name the pattern just bound.

Guards are also the cleanest way to compare against a runtime variable when a dotted name isn't available:

case name if name == preferred_category: binds name (capture pattern), then checks the guard. Without the guard, writing case preferred_category: would silently rebind preferred_category instead of comparing against it.

Guards run after the structural match, so they only see the case body's local bindings, not the original subject (use the bound names instead). They're also evaluated lazily; later cases are still tried if an earlier guard returns false.

Each pattern is identical in shape ({"items": items}), but the guards split them apart by the captured value. This is a common idiom: use the pattern for shape, use the guard for values that need an expression beyond simple equality.

Why match Isn't Just an if/elif Chain

By now you've seen what match does that if/elif doesn't. Three things make it qualitatively different:

1. Structural decomposition. A single case checks the shape of a value and pulls out its parts at the same time. The equivalent if/elif needs isinstance plus key/attribute access plus value comparisons stacked together.

2. Binding. When a pattern matches, the inner values are already in named local variables in the case body. You don't write total = event["total"] or total = order.total after the check; the pattern does it.

3. One subject evaluation. The expression after match is evaluated exactly once, no matter how many cases follow. An elif chain on a computed value usually re-evaluates the value or saves it to a temporary, which is more code to read.

Put together, match shines when your data has shape: dicts with varying keys, lists with structural meaning (head/tail), or class hierarchies. For plain "if x is 1, elif x is 2" code, an if/elif chain is fine and often clearer. Reach for match when you'd otherwise write nested isinstance checks or repeated key access.

A side-by-side comparison:

Concernif/elifmatch/case
Compare a value to constantsCleanClean (literal patterns)
Check type and extract fieldsVerbose (isinstance + getattr)One pattern
Check shape of nested dictVerbose (in + indexing)One pattern
Bind extracted valuesManual (x = event["x"])Automatic
Compare against a runtime variableDirect (if x == VAR:)Needs dotted name or guard
One-time subject evaluationManual (temp variable)Built in

Use match when you'd otherwise write nested type and shape checks. Use if/elif for plain value comparisons against runtime variables, where the capture pitfall would be a foot-gun.

Quiz

match-case (Python 3.10+) Quiz

10 quizzes