Python match-case: Pattern Matching with Guards and Real Examples

Python course module guide: Control structures

Python match-case is a control-flow tool for branching on the shape and contents of structured data. It can make parsers, command handlers and event routers clearer than a long chain of type checks, but only when each case expresses a real pattern.

What you will learn

  • Choose between match and if/elif.
  • Use literal, OR, sequence, mapping and class patterns.
  • Add guards without hiding business rules.
  • Avoid capture-pattern and ordering mistakes.

What Python match-case does

A match statement evaluates one subject, tries cases from top to bottom and runs the first case whose pattern and optional guard succeed. The underscore pattern is commonly used as the final catch-all. Unlike a simple switch statement, structural pattern matching can unpack sequences, read selected mapping keys and match class attributes.

Use it when one value can arrive in several recognizable shapes. Examples include commands, API events, parsed records and workflow states. For ordinary numeric ranges or unrelated Boolean conditions, if/elif often remains easier to read. Review the broader Python control structures guide before treating match as a replacement for every conditional.

Start with literal and OR patterns

def access_message(role: str) -> str:
    match role.lower():
        case "admin":
            return "Full access"
        case "editor" | "reviewer":
            return "Content access"
        case "student":
            return "Learning access"
        case _:
            return "Unknown role"

The OR pattern keeps alternatives that have the same outcome together. Put specific cases first and the wildcard last. A wildcard placed early would match everything and make later cases unreachable.

Unpack sequence patterns

Sequence patterns are useful for tokenized commands. The pattern can require a fixed length or capture remaining values with a starred name.

def run_command(parts: list[str]) -> str:
    match parts:
        case ["add", name, score]:
            return f"Add {name} with score {score}"
        case ["remove", name]:
            return f"Remove {name}"
        case ["report", *subjects] if subjects:
            return f"Report for {', '.join(subjects)}"
        case _:
            return "Invalid command"

This style works well after input has been split and normalized. It should not replace validation: the captured score is still text and must be converted safely before calculation.

Match dictionaries from APIs

Mapping patterns let you require only the keys needed by a branch. Extra keys are allowed unless your logic explicitly handles them.

def route_event(event: dict) -> str:
    match event:
        case {"type": "payment", "amount": amount} if amount > 0:
            return f"Process payment: {amount}"
        case {"type": "refund", "order_id": order_id}:
            return f"Review refund for {order_id}"
        case {"type": event_type}:
            return f"Unsupported event: {event_type}"
        case _:
            return "Malformed event"

Real API clients also need timeouts, status checks and schema validation. The later Python API pagination and retries guide shows the network side of that workflow.

Use guards for extra conditions

A guard is an if expression attached to a case. The pattern must match before the guard runs. Guards are useful for small constraints such as a positive amount or an allowed status. If a guard contains several database calls or complex calculations, move that logic into a named function so the branch remains understandable.

Class patterns for domain objects

from dataclasses import dataclass

@dataclass
class Order:
    status: str
    total: float

def next_action(order: Order) -> str:
    match order:
        case Order(status="paid", total=total) if total >= 5000:
            return "priority dispatch"
        case Order(status="paid"):
            return "standard dispatch"
        case Order(status="cancelled"):
            return "stop fulfilment"
        case _:
            return "manual review"

Class patterns are most valuable when the objects already represent meaningful domain concepts. If classes and objects are new, first study Python OOP with practical examples.

Common match-case mistakes

  • Using a bare name as a constant: a bare name usually captures a value. Use literals or qualified enum members for constants.
  • Putting a catch-all too early: cases are checked in order.
  • Skipping validation: matching a shape does not prove that every captured value has the correct business meaning.
  • Forcing simple conditions into patterns: an ordinary if may communicate the rule better.
  • Ignoring the Python version: structural pattern matching requires Python 3.10 or later.

Practice exercise: order event router

Create a function that receives a dictionary for order creation, payment, cancellation or refund. Use mapping patterns to capture the required identifiers, a guard to reject non-positive amounts and a final case for malformed input. Write at least one test for every branch. When invalid events deserve distinct messages, connect the router to the Python custom exceptions guide.

When match-case is the right choice

Choose match when the alternatives describe shapes: different command layouts, event schemas or domain objects. Choose if/elif when the alternatives are mainly calculations, ranges or independent Boolean expressions. The best control structure is the one a teammate can verify quickly.

Python match-case FAQs

Which Python version supports match-case?

Structural pattern matching was introduced in Python 3.10. Confirm the runtime version before using it in a project or deployment environment.

Does match-case replace dictionaries of functions?

No. A dispatch dictionary is often ideal for a simple command-to-function mapping. Match-case becomes stronger when input has several shapes or needs unpacking and guards.

Can a case continue into the next case?

No. Python does not use switch-style fall-through. The first successful pattern and guard runs, and the match statement then ends.

Make captures, constants and validation explicit

A bare identifier in a pattern normally captures the subject; it does not compare the subject with a variable that happened to have that name earlier. Naming a variable in uppercase does not change this rule. Use a literal for a fixed value or a qualified constant, such as an enum member accessed through its class.

An unguarded capture or wildcard is irrefutable. Placing one before later cases can therefore produce a SyntaxError for unreachable cases rather than a useful runtime fallback. Keep the final catch-all at the end. When debugging, distinguish code that cannot compile from a valid pattern that matches more inputs than intended.

A validated event router with observable cases

The earlier payment example assumes that its amount is comparable with zero. Real external input may instead contain a string, a missing value or a Boolean. A guard can raise an exception; a failed operation inside the guard is not automatically treated as a failed match. Validate types before performing the comparison.

This separate demonstration represents money as integer cents to keep the input contract narrow. It is a routing exercise rather than a payment implementation. Notice the explicit rejection of Boolean values, which are instances of the integer type in Python.

def route(message):
    match message:
        case {"type": "payment", "cents": int(cents)} if type(cents) is int and cents > 0:
            return f"payment {cents}"
        case {"type": "payment"}:
            return "invalid payment"
        case ["report", *names] if names and all(isinstance(name, str) for name in names):
            return f"report {len(names)}"
        case _:
            return "unsupported shape"

print(route({"type": "payment", "cents": 2500, "source": "demo"}))
print(route({"type": "payment", "cents": True}))
print(route({"type": "payment", "cents": "2500"}))
print(route(["report", "Asha", "Ravi"]))
print(route("report"))

Observed output using Python 3.12.14:

payment 2500
invalid payment
invalid payment
report 2
unsupported shape

The first mapping includes an extra source key and still matches. The Boolean and string examples reach the invalid-payment branch. The report list matches a sequence shape, but the plain string does not. These observed results test the boundary of the patterns, not just the successful path.

Sequence patterns are not a generic iteration request

A list-shaped pattern can match suitable sequences such as lists and tuples; it is not restricted to the bracket style used in the pattern. However, strings, bytes and bytearrays are deliberately excluded from sequence-pattern matching. An ordinary iterator or generator is also not automatically consumed to satisfy such a pattern.

If the application receives a text command, tokenise it deliberately before matching the resulting sequence. Decide how quoted names, spaces and missing arguments should be handled. Calling a basic split may be enough for a small exercise but is not a complete command-language parser. Keep tokenisation and routing as separate, testable responsibilities.

A starred capture collects the remaining sequence elements, so a report pattern alone can accept zero names. The guard in the example rules out that case and checks that every supplied name is a string. Each requirement should be visible: shape, non-empty payload and element types are separate conditions.

Mapping patterns accept extra information by default

Requiring the type and cents keys does not prohibit additional keys. This is useful for versioned events that carry optional metadata, but it can be too permissive for a strict input contract. If unknown fields must be rejected, capture or inspect the remaining fields and validate them explicitly. Do not assume a successful mapping pattern is equivalent to complete schema validation.

Order specialised cases before broad ones. In the demonstration, a valid payment is tested before the broader payment-type branch. Moving the broad branch first would send valid payments to the invalid result. This ordering problem differs from the compile-time irrefutable-capture error: the program can run while selecting the wrong meaningful branch.

Are captured values safe to use after the statement?

A successful case can bind names that remain available in the surrounding scope. Avoid relying on names from failed partial matches or branches that did not run. Return a deliberate result from each branch when possible.

What should the router tests cover?

Include a valid mapping, extra keys, missing fields, incorrect types, zero and negative values, a tuple command, a plain string and an iterator. Check both accepted and rejected shapes so a concise router does not become a permissive input validator by accident.

Further official documentation: Python structural pattern matching reference; Python tutorial pattern matching examples.

Official reference: Python language reference for the match statement.

Learn Python with guided practice in Visakhapatnam

These concepts become useful when you apply them in exercises, assignments and reviewed projects. Explore Python training in Vizag for the complete curriculum, classroom and online learning options, and current batch details.

Python module learning path

Continue through the related practical guides in this course-module series:

  1. Python match-case pattern matching
  2. Python list comprehensions
  3. Python iterators and generators
  4. Python decorators
  5. Python context managers
  6. Python custom exceptions
  7. pytest unit testing for beginners
  8. Python API pagination and retries
  9. pandas GroupBy and merge
  10. Flask REST API with SQLite

Leave a Comment

Your email address will not be published. Required fields are marked *