DEV Community

Cover image for Python by Structure: Match Statements and Pattern Guards
Aaron Rose
Aaron Rose

Posted on

Python by Structure: Match Statements and Pattern Guards

Timothy stared at a function that had grown into a monster of nested if/elif statements. "Margaret, this works, but it's getting hard to read. I'm checking the type, then the value, then special cases... is there a better way?"

Margaret looked at his code:

def process_command(cmd):
    if isinstance(cmd, dict):
        if "action" in cmd and cmd["action"] == "start":
            if "delay" in cmd and cmd["delay"] > 0:
                return schedule_start(cmd["delay"])
            else:
                return start_now()
        elif "action" in cmd and cmd["action"] == "stop":
            return stop()
    elif isinstance(cmd, str):
        if cmd == "help":
            return show_help()
        elif cmd == "status":
            return get_status()
    return "Unknown command"
Enter fullscreen mode Exit fullscreen mode

"This is a perfect case for Python's match statement," Margaret said. "Python 3.10 added it for exactly this - pattern matching with conditions."

"Match statement?" Timothy asked. "I've seen it in the docs but never understood when to use it."

The Problem: Nested Type and Value Checking

"Let me show you," Margaret said, rewriting the function:

def process_command(cmd):
    match cmd:
        case {"action": "start", "delay": d} if d > 0:
            return schedule_start(d)
        case {"action": "start"}:
            return start_now()
        case {"action": "stop"}:
            return stop()
        case "help":
            return show_help()
        case "status":
            return get_status()
        case _:
            return "Unknown command"
Enter fullscreen mode Exit fullscreen mode

"Wait," Timothy said. "That's... way cleaner. But what's the if d > 0 doing in the middle of the case?"

"That's a guard," Margaret explained. "The syntax is case <pattern> if <guard_expression>: - the guard is any Python expression that returns a boolean, and it's evaluated only if the pattern successfully matches. Let me show you the structure."

Tree View:

process_command(cmd)
    Match cmd
        Case {'action': 'start', 'delay': d} if d > 0
            Return schedule_start(d)
        Case {'action': 'start'}
            Return start_now()
        Case {'action': 'stop'}
            Return stop()
        Case 'help'
            Return show_help()
        Case 'status'
            Return get_status()
        Case _
            Return 'Unknown command'
Enter fullscreen mode Exit fullscreen mode

English View:

Function process_command(cmd):
  Match cmd:
    Case {'action': 'start', 'delay': d} if d > 0:
      Return schedule_start(d).
    Case {'action': 'start'}:
      Return start_now().
    Case {'action': 'stop'}:
      Return stop().
    Case 'help':
      Return show_help().
    Case 'status':
      Return get_status().
    Case _:
      Return 'Unknown command'.
Enter fullscreen mode Exit fullscreen mode

Timothy studied the structure. "So the first case matches a dictionary with 'action' and 'delay', extracts the delay value as d, and THEN checks if it's greater than zero?"

"Exactly," Margaret confirmed. "The pattern matches the structure - is it a dict? Does it have these keys? If yes, bind the values to variables. Then the guard (if d > 0) adds an additional condition. If the guard fails, Python tries the next case."

"That's why the second case is just {'action': 'start'} without the delay," Timothy observed. "If the first case's guard fails, it falls through to this one?"

"Precisely. The structure shows the decision tree clearly - pattern first, then guard, then action."

Understanding Pattern Guards

Margaret pulled up a simpler example to demonstrate guards:

def categorize_number(x):
    match x:
        case n if n < 0:
            return "negative"
        case 0:
            return "zero"
        case n if n > 100:
            return "too large"
        case n:
            return "valid"
Enter fullscreen mode Exit fullscreen mode

Tree View:

categorize_number(x)
    Match x
        Case n if n < 0
            Return 'negative'
        Case 0
            Return 'zero'
        Case n if n > 100
            Return 'too large'
        Case n
            Return 'valid'
Enter fullscreen mode Exit fullscreen mode

English View:

Function categorize_number(x):
  Match x:
    Case n if n < 0:
      Return 'negative'.
    Case 0:
      Return 'zero'.
    Case n if n > 100:
      Return 'too large'.
    Case n:
      Return 'valid'.
Enter fullscreen mode Exit fullscreen mode

"See how the structure shows the decision flow?" Margaret pointed at the cases. "First case: bind to n, check if negative. Second case: exact match for zero. Third case: bind to n, check if too large. Final case: anything else."

Timothy compared it to how he would have written it:

def categorize_number_old(x):
    if x < 0:
        return "negative"
    elif x == 0:
        return "zero"
    elif x > 100:
        return "too large"
    else:
        return "valid"
Enter fullscreen mode Exit fullscreen mode

Tree View:

categorize_number_old(x)
    If x < 0
    └── Return 'negative'
    Elif x == 0
    └── Return 'zero'
    Elif x > 100
    └── Return 'too large'
    Else
        Return 'valid'
Enter fullscreen mode Exit fullscreen mode

English View:

Function categorize_number_old(x):
  If x < 0:
    Return 'negative'.
  Elif x == 0:
    Return 'zero'.
  Elif x > 100:
    Return 'too large'.
  Else:
    Return 'valid'.
Enter fullscreen mode Exit fullscreen mode

"The if/elif version works," Margaret said, "but the match version makes the intent clearer - you're categorizing a value based on patterns and conditions. The structure shows you're matching against different cases, not just testing conditions. And remember, just like if/elif, case order matters. The most specific patterns - like case 0: - must come before the less specific patterns - like case n: - to ensure they're checked first."

Pattern Matching Beyond Guards

"What else can you pattern match?" Timothy asked.

"Lots of structures," Margaret said. "Lists, tuples, even complex nested patterns:"

def process_data(data):
    match data:
        case []:
            return "empty"
        case [x]:
            return f"single: {x}"
        case [x, y]:
            return f"pair: {x}, {y}"
        case [first, *rest] if len(rest) > 5:
            return f"long list starting with {first}"
        case [first, *rest]:
            return f"list starting with {first}"
Enter fullscreen mode Exit fullscreen mode

Tree View:

process_data(data)
    Match data
        Case []
            Return 'empty'
        Case [x]
            Return f'single: {x}'
        Case [x, y]
            Return f'pair: {x}, {y}'
        Case [first, *rest] if len(rest) > 5
            Return f'long list starting with {first}'
        Case [first, *rest]
            Return f'list starting with {first}'
Enter fullscreen mode Exit fullscreen mode

English View:

Function process_data(data):
  Match data:
    Case []:
      Return 'empty'.
    Case [x]:
      Return f'single: {x}'.
    Case [x, y]:
      Return f'pair: {x}, {y}'.
    Case [first, *rest] if len(rest) > 5:
      Return f'long list starting with {first}'.
    Case [first, *rest]:
      Return f'list starting with {first}'.
Enter fullscreen mode Exit fullscreen mode

"Look at the fourth case," Margaret pointed. "It matches a list with at least one element, captures the first as first and the rest as rest, then the guard checks if the rest has more than 5 items. Pattern matching plus conditional logic."

Timothy's eyes lit up. "So you can match the shape of the data - empty list, single item, pair, longer list - and also add conditions with guards?"

"Exactly. The match statement separates structure matching from value conditions. The pattern describes what shape you're looking for. The guard adds 'and also this must be true.'"

Margaret showed one more example with dictionaries:

def handle_event(event):
    match event:
        case {"type": "click", "button": btn} if btn == "left":
            return handle_left_click()
        case {"type": "click", "button": "right"}:
            return handle_right_click()
        case {"type": "keypress", "key": k} if k.isupper():
            return f"Uppercase: {k}"
        case {"type": "keypress", "key": k}:
            return f"Lowercase: {k}"
        case _:
            return "Unknown event"
Enter fullscreen mode Exit fullscreen mode

Tree View:

handle_event(event)
    Match event
        Case {'type': 'click', 'button': btn} if btn == 'left'
            Return handle_left_click()
        Case {'type': 'click', 'button': 'right'}
            Return handle_right_click()
        Case {'type': 'keypress', 'key': k} if k.isupper()
            Return f'Uppercase: {k}'
        Case {'type': 'keypress', 'key': k}
            Return f'Lowercase: {k}'
        Case _
            Return 'Unknown event'
Enter fullscreen mode Exit fullscreen mode

English View:

Function handle_event(event):
  Match event:
    Case {'type': 'click', 'button': btn} if btn == 'left':
      Return handle_left_click().
    Case {'type': 'click', 'button': 'right'}:
      Return handle_right_click().
    Case {'type': 'keypress', 'key': k} if k.isupper():
      Return f'Uppercase: {k}'.
    Case {'type': 'keypress', 'key': k}:
      Return f'Lowercase: {k}'.
    Case _:
      Return 'Unknown event'.
Enter fullscreen mode Exit fullscreen mode

"The structure makes the event handling logic obvious," Margaret said. "Each case describes an event type and structure, guards add specific conditions, and the underscore catches everything else."

Timothy looked back at his original nested if/elif monster. "So instead of checking isinstance, then checking keys, then checking values all in separate conditions, I describe the whole pattern I'm looking for and add a guard if needed?"

"That's the power of match statements," Margaret said. "The structure reveals your intent - you're matching patterns, not just testing conditions. And guards let you add 'and also check this' without nesting another level of if statements."

Timothy refactored his command processor, watching as the deeply nested conditionals flattened into a clean match statement with clear cases and guards. "The structure shows exactly what I'm matching for. It's like a decision tree that reads top to bottom."

"Now you're thinking in patterns," Margaret smiled. "Match statements are Python 3.10's way of making complex conditional logic both powerful and readable."


Explore Python structure yourself: Download the Python Structure Viewer - a free tool that shows code structure in tree and plain English views. Works offline, no installation required.

Python Structure Viewer


Aaron Rose is a software engineer and technology writer at tech-reader.blog and the author of Think Like a Genius.

Top comments (0)