Skip to main content

Python Structural Pattern Matching: Practice Problems & Exercises

Practice: Structural Pattern Matching

12 problems4 Easy5 Medium3 Hard45–60 min
← Back to lesson

Easy

#1HTTP Status Code ClassifierEasy
match-caseliteral-patternbasics

Write a function that classifies HTTP status codes using match/case with literal patterns. Map 200, 301, 403, 404, and 500 to their standard names. Everything else should return "Unknown status code".

Python
def classify_status(code):
    match code:
        case 200:
            return "OK"
        case 301:
            return "Moved Permanently"
        case 403:
            return "Forbidden"
        case 404:
            return "Not Found"
        case 500:
            return "Internal Server Error"
        case _:
            return "Unknown status code"

# --- Test it ---
test_codes = [200, 301, 404, 500, 418, 201, 403]
for code in test_codes:
    print(f"{code} -> {classify_status(code)}")
Solution
def classify_status(code):
match code:
case 200:
return "OK"
case 301:
return "Moved Permanently"
case 403:
return "Forbidden"
case 404:
return "Not Found"
case 500:
return "Internal Server Error"
case _:
return "Unknown status code"

Key concepts — literal patterns:

  • Each case 200: is a literal pattern — it matches only when the subject equals that exact value.
  • Literal patterns work with integers, strings, booleans, None, and True/False.
  • The case _: wildcard pattern matches anything. It must be the last case — Python evaluates cases top to bottom and stops at the first match.
  • Unlike C's switch, there is no fall-through. Each case is independent — no break needed.

Why match/case over if/elif?

For simple value dispatch like this, match/case and if/elif are roughly equivalent. The real power of match/case shows up in the Medium and Hard problems where you destructure sequences, mappings, and objects.

Expected Output
200 -> OK
301 -> Moved Permanently
404 -> Not Found
500 -> Internal Server Error
418 -> Unknown status code
201 -> Unknown status code
403 -> Forbidden
Hints

Hint 1: Use a match statement with literal integer values as case patterns. Each case matches an exact value.

Hint 2: The case _ pattern is the wildcard — it matches anything not caught by earlier cases, like the else in an if/elif chain.

#2Capture Pattern — Greeting RouterEasy
match-casecapture-patternvariable-binding

Build a greeting router that uses capture patterns to extract a name from a command tuple. Match on the action literal while capturing the name into a variable.

Python
def route_greeting(command):
    match command:
        case ("hello", name):
            return f"Hello, {name}!"
        case ("goodbye", name):
            return f"Goodbye, {name}. See you later!"
        case (action, name):
            return f"Hey {name}, I don't know how to: {action}"

# --- Test it ---
commands = [
    ("hello", "Alice"),
    ("goodbye", "Bob"),
    ("hello", "World"),
    ("thanks", "Charlie"),
]

for cmd in commands:
    print(f"{cmd} -> {route_greeting(cmd)}")
Solution
def route_greeting(command):
match command:
case ("hello", name):
return f"Hello, {name}!"
case ("goodbye", name):
return f"Goodbye, {name}. See you later!"
case (action, name):
return f"Hey {name}, I don't know how to: {action}"

How capture patterns work:

  1. case ("hello", name)"hello" is a literal pattern (must match exactly), name is a capture pattern (binds whatever value is in that position).
  2. case (action, name) — both are capture patterns. This matches ANY 2-element tuple and binds both values. It acts as a catch-all for tuples.
  3. Python matches top to bottom. The first case that matches wins. So the specific cases ("hello", "goodbye") must come before the generic (action, name) catch-all.

Common mistake — name shadowing:

# WRONG — this does NOT match the variable `expected_action`
expected_action = "hello"
match command:
case (expected_action, name): # This CAPTURES, not compares!
...

A bare name in a pattern is always a capture. To match against an existing variable, use a guard clause: case (action, name) if action == expected_action:.

Expected Output
('hello', 'Alice') -> Hello, Alice!
('goodbye', 'Bob') -> Goodbye, Bob. See you later!
('hello', 'World') -> Hello, World!
('thanks', 'Charlie') -> Hey Charlie, I don't know how to: thanks
Hints

Hint 1: A bare name in a case pattern (like `case ("hello", name)`) is a capture pattern — it binds the matched value to that variable for use in the case body.

Hint 2: Be careful: capture patterns always match. If you write `case (action, name):` without a literal, it captures both values and always succeeds.

#3OR Patterns — Day Type ClassifierEasy
match-caseor-patterncombining-cases

Classify days of the week using OR patterns (|) to group multiple literals into a single case. Weekdays and weekends should each be a single case.

Python
def classify_day(day):
    match day:
        case "Monday" | "Tuesday" | "Wednesday" | "Thursday" | "Friday":
            return "Weekday"
        case "Saturday" | "Sunday":
            return "Weekend"
        case _:
            return "Not a valid day"

# --- Test it ---
days = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday",
        "Saturday", "Sunday", "Holiday"]

for day in days:
    print(f"{day:9s} -> {classify_day(day)}")
Solution
def classify_day(day):
match day:
case "Monday" | "Tuesday" | "Wednesday" | "Thursday" | "Friday":
return "Weekday"
case "Saturday" | "Sunday":
return "Weekend"
case _:
return "Not a valid day"

OR patterns with |:

  • The | operator combines multiple patterns into one case. If ANY of the alternatives match, the case body executes.
  • This is much cleaner than the equivalent if/elif:
# Without match — verbose and repetitive
if day in ("Monday", "Tuesday", "Wednesday", "Thursday", "Friday"):
return "Weekday"
elif day in ("Saturday", "Sunday"):
return "Weekend"

Rules for OR patterns:

  1. All alternatives must be the same kind of pattern. You cannot mix a literal with a capture on different sides of |.
  2. If any alternative uses a capture variable, ALL alternatives must capture the same variable names. For example: case ("a", x) | ("b", x): is valid — both sides capture x. But case ("a", x) | ("b", y): is invalid.
  3. OR patterns work at any nesting level — you can use them inside sequence patterns, mapping patterns, and class patterns.
Expected Output
Monday    -> Weekday
Tuesday   -> Weekday
Wednesday -> Weekday
Thursday  -> Weekday
Friday    -> Weekday
Saturday  -> Weekend
Sunday    -> Weekend
Holiday   -> Not a valid day
Hints

Hint 1: Use the | (pipe) operator inside a case to combine multiple patterns: case "Saturday" | "Sunday": matches either value.

Hint 2: OR patterns can combine any pattern types — literals, capture patterns, even class patterns. All alternatives must bind the same set of variables (or none).

#4Wildcard Default — Calculator OperatorEasy
match-casewildcarddefault-case

Build a basic calculator using match/case. Match on the operator and capture both operands. Handle division by zero and unknown operators with the wildcard default.

Python
def calculate(expression):
    match expression:
        case (left, "+", right):
            return left + right
        case (left, "-", right):
            return left - right
        case (left, "*", right):
            return left * right
        case (left, "/", right) if right != 0:
            return left / right
        case (_, "/", 0):
            return "Error: Division by zero"
        case (_, op, _):
            return f"Error: Unknown operator '{op}'"

# --- Test it ---
expressions = [
    (10, "+", 5),
    (10, "-", 5),
    (10, "*", 5),
    (10, "/", 5),
    (10, "/", 0),
    (10, "%", 5),
    (10, "**", 5),
]

for expr in expressions:
    result = calculate(expr)
    print(f"{str(expr):13s} -> {result}")
Solution
def calculate(expression):
match expression:
case (left, "+", right):
return left + right
case (left, "-", right):
return left - right
case (left, "*", right):
return left * right
case (left, "/", right) if right != 0:
return left / right
case (_, "/", 0):
return "Error: Division by zero"
case (_, op, _):
return f"Error: Unknown operator '{op}'"

Multiple pattern features working together:

  1. Sequence pattern: (left, "+", right) matches a 3-element tuple and destructures it.
  2. Literal pattern: "+" matches the exact string.
  3. Capture pattern: left and right bind the operand values.
  4. Guard clause: if right != 0 adds an extra condition beyond structural matching.
  5. Wildcard: _ discards values we do not need (the operands in the error cases).

Order matters for division:

The guarded case case (left, "/", right) if right != 0: must come BEFORE case (_, "/", 0):. If the guard fails (because right is 0), Python moves to the next case. This two-case pattern for division is idiomatic — guard for the happy path, then catch the error case.

Wildcard _ vs capture:

  • _ means "match anything but throw it away" — it does not create a variable.
  • A named capture like op in case (_, op, _): binds the value so you can use it in the case body.
  • You can use _ multiple times in a single pattern. You cannot use the same capture name twice.
Expected Output
(10, '+', 5)  -> 15
(10, '-', 5)  -> 5
(10, '*', 5)  -> 50
(10, '/', 5)  -> 2.0
(10, '/', 0)  -> Error: Division by zero
(10, '%', 5)  -> Error: Unknown operator '%'
(10, '**', 5) -> Error: Unknown operator '**'
Hints

Hint 1: Use literal patterns for the operator string and capture patterns for the operands. The wildcard _ handles unknown operators.

Hint 2: For division by zero, you can use a guard clause (if right != 0) or handle it in the case body with a conditional.


Medium

#5Sequence Destructuring — Point ClassifierMedium
match-casesequence-patterndestructuring

Classify coordinate points by matching on their structure. Detect the origin, axis-aligned points, and arbitrary n-dimensional points using sequence patterns.

Python
def classify_point(coords):
    match coords:
        case []:
            return "Empty: no coordinates"
        case [0]:
            return "Origin in 1D"
        case [x]:
            return f"Point on number line: x={x}"
        case [0, 0]:
            return "Origin in 2D"
        case [x, 0]:
            return f"On X-axis at x={x}"
        case [0, y]:
            return f"On Y-axis at y={y}"
        case [x, y]:
            return f"2D point at ({x}, {y})"
        case [0, 0, 0]:
            return "Origin in 3D"
        case [x, y, z]:
            return f"3D point at ({x}, {y}, {z})"
        case [*rest]:
            return f"Higher-dimensional point with {len(rest)} coords: {rest}"

# --- Test it ---
test_points = [
    [], [0], [5], [0, 0], [3, 0], [0, 7],
    [3, 4], [0, 0, 0], [1, 2, 3], [1, 2, 3, 4],
]

for point in test_points:
    label = str(point)
    print(f"{label:11s} -> {classify_point(point)}")
Solution
def classify_point(coords):
match coords:
case []:
return "Empty: no coordinates"
case [0]:
return "Origin in 1D"
case [x]:
return f"Point on number line: x={x}"
case [0, 0]:
return "Origin in 2D"
case [x, 0]:
return f"On X-axis at x={x}"
case [0, y]:
return f"On Y-axis at y={y}"
case [x, y]:
return f"2D point at ({x}, {y})"
case [0, 0, 0]:
return "Origin in 3D"
case [x, y, z]:
return f"3D point at ({x}, {y}, {z})"
case [*rest]:
return f"Higher-dimensional point with {len(rest)} coords: {rest}"

Sequence pattern features:

  1. Fixed-length matching: [x, y] matches any sequence of exactly 2 elements.
  2. Literal inside sequence: [0, 0] matches only when both elements are zero.
  3. Mixed literal + capture: [x, 0] captures the first element but requires the second to be zero.
  4. Star pattern: [*rest] matches any number of remaining elements, like *args in function definitions.

Order is critical:

  • [0, 0] must come before [x, 0], which must come before [x, y]. More specific patterns first.
  • If [x, y] came first, it would capture [0, 0] because x=0, y=0 is a valid binding.
  • Think of it like exception handling — catch the specific cases before the general ones.

Sequence patterns match lists AND tuples:

Both [3, 4] and (3, 4) match the pattern [x, y]. Python's sequence patterns are structural — they check for sequence protocol, not the specific type. Use a class pattern if you need to distinguish lists from tuples.

Expected Output
[]          -> Empty: no coordinates
[0]         -> Origin in 1D
[5]         -> Point on number line: x=5
[0, 0]      -> Origin in 2D
[3, 0]      -> On X-axis at x=3
[0, 7]      -> On Y-axis at y=7
[3, 4]      -> 2D point at (3, 4)
[0, 0, 0]   -> Origin in 3D
[1, 2, 3]   -> 3D point at (1, 2, 3)
[1, 2, 3, 4] -> Higher-dimensional point with 4 coords: [1, 2, 3, 4]
Hints

Hint 1: Sequence patterns match lists and tuples. Use [x] for 1 element, [x, y] for 2, [x, y, z] for 3. Use [*rest] to capture variable-length sequences.

Hint 2: Match literal 0 in specific positions to detect axes: [x, 0] means "on the X-axis". Order cases from most specific to most general.

#6Mapping Patterns — Config ParserMedium
match-casemapping-patterndict-matching

Parse configuration dictionaries using mapping patterns. Different config shapes should route to different handlers based on their keys and values.

Python
def parse_config(config):
    match config:
        case {"type": "database", "host": host, "port": port}:
            return f"Database config -> host={host}, port={port}"
        case {"type": "cache", "backend": "redis", "url": url, "ttl": ttl}:
            return f"Redis cache config -> {url}, ttl={ttl}s"
        case {"type": "api", "method": method, "url": url}:
            return f"API endpoint -> {method} {url}"
        case {"type": kind}:
            return f"Unknown config type: {kind}"

# --- Test it ---
configs = [
    {"type": "database", "host": "localhost", "port": 5432, "name": "mydb"},
    {"type": "database", "host": "db.prod.com", "port": 3306},
    {"type": "cache", "backend": "redis", "url": "redis://cache:6379", "ttl": 300},
    {"type": "api", "method": "GET", "url": "https://api.example.com/users"},
    {"type": "logging", "level": "DEBUG"},
]

for i, cfg in enumerate(configs, 1):
    print(f"Config {i}: {parse_config(cfg)}")
Solution
def parse_config(config):
match config:
case {"type": "database", "host": host, "port": port}:
return f"Database config -> host={host}, port={port}"
case {"type": "cache", "backend": "redis", "url": url, "ttl": ttl}:
return f"Redis cache config -> {url}, ttl={ttl}s"
case {"type": "api", "method": method, "url": url}:
return f"API endpoint -> {method} {url}"
case {"type": kind}:
return f"Unknown config type: {kind}"

How mapping patterns work:

  1. Keys must be literals: "type", "host", "port" — you cannot use variables as keys in a pattern.
  2. Values can be anything: Literals ("redis"), captures (host), or even nested patterns.
  3. Extra keys are allowed: Config 1 has a "name" key that the pattern ignores. Mapping patterns match if the specified keys are present — they do not require an exact match. This is intentional and very useful for real-world configs that evolve over time.
  4. Use **rest to capture extra keys: case {"type": "db", **rest}: captures all unmatched keys into rest.

Mapping vs sequence patterns — a critical difference:

Sequence [x, y] -> matches EXACTLY 2 elements
Mapping {"a": x} -> matches if "a" key EXISTS (extra keys OK)

This asymmetry is by design. Sequences are positional (order and count matter), while mappings are key-based (only presence matters). Extra keys in a dict are the norm in real APIs and configs.

Production pattern — versioned config:

match config:
case {"version": 2, "database": {"host": h, "port": p}}:
... # Handle v2 format with nested dict
case {"version": 1, "db_host": h, "db_port": p}:
... # Handle legacy v1 flat format
Expected Output
Config 1: Database config -> host=localhost, port=5432
Config 2: Database config -> host=db.prod.com, port=3306
Config 3: Redis cache config -> redis://cache:6379, ttl=300s
Config 4: API endpoint -> GET https://api.example.com/users
Config 5: Unknown config type: logging
Hints

Hint 1: Mapping patterns use {"key": pattern} syntax to match dictionaries. Keys must be literals, but values can be captures, literals, or nested patterns.

Hint 2: Mapping patterns match even if the dict has EXTRA keys beyond what the pattern specifies. This is different from sequence patterns which require an exact length match.

#7Guard Clauses — Grade ClassifierMedium
match-caseguard-clauseconditional-matching

Classify exam scores into letter grades using guard clauses. Handle out-of-range scores, perfect scores, and the standard A/B/C/D/F grading scale.

Python
def classify_grade(score):
    match score:
        case s if s < 0 or s > 100:
            return f"Invalid score: {s}"
        case 100:
            return "A+ (Perfect score!)"
        case s if s >= 90:
            return "A  (Excellent)"
        case s if s >= 80:
            return "B  (Good)"
        case s if s >= 70:
            return "C  (Average)"
        case s if s >= 60:
            return "D  (Below Average)"
        case s:
            return "F  (Failing)"

# --- Test it ---
test_scores = [95, 85, 75, 65, 45, 100, -5, 105]

for score in test_scores:
    print(f"{score:4d} -> {classify_grade(score)}")
Solution
def classify_grade(score):
match score:
case s if s < 0 or s > 100:
return f"Invalid score: {s}"
case 100:
return "A+ (Perfect score!)"
case s if s >= 90:
return "A (Excellent)"
case s if s >= 80:
return "B (Good)"
case s if s >= 70:
return "C (Average)"
case s if s >= 60:
return "D (Below Average)"
case s:
return "F (Failing)"

Guard clause mechanics:

  1. The pattern case s always matches and captures the value into s. The if guard then adds a condition.
  2. Guards are evaluated only if the pattern matches. Pattern matching and guard evaluation are two separate steps.
  3. If the guard is False, Python moves to the next case — it does NOT raise an error.

Order matters with guards:

case s if s < 0 or s > 100: <- bounds check FIRST
case 100: <- exact match for perfect score
case s if s >= 90: <- ranges from high to low
case s if s >= 80:
case s if s >= 70:
case s if s >= 60:
case s: <- catch-all (no guard = always matches)

If you put case s: (no guard) anywhere except last, it would swallow all remaining values.

Guards vs pattern conditions:

Guards can reference any variable in scope, not just captured names. You could write case s if s >= threshold: where threshold is an outer variable. This is how you match against runtime values — something literal patterns alone cannot do.

Expected Output
  95 -> A  (Excellent)
  85 -> B  (Good)
  75 -> C  (Average)
  65 -> D  (Below Average)
  45 -> F  (Failing)
 100 -> A+ (Perfect score!)
  -5 -> Invalid score: -5
 105 -> Invalid score: 105
Hints

Hint 1: Guard clauses use `if` after the pattern: `case score if score >= 90:`. The pattern must match first, THEN the guard is evaluated.

Hint 2: Order guard clauses from most specific to most general. Place the bounds check (score < 0 or score > 100) first, or use it as the final catch-all.

#8Class Pattern — Shape Area CalculatorMedium
match-caseclass-patterndataclass

Calculate areas of different shapes using class patterns with dataclasses. Match on the shape type and destructure fields in one step. Detect special cases like squares.

Python
from dataclasses import dataclass
import math

@dataclass
class Circle:
    radius: float

@dataclass
class Rectangle:
    width: float
    height: float

@dataclass
class Triangle:
    base: float
    height: float

def compute_area(shape):
    match shape:
        case Circle(radius=r):
            area = math.pi * r ** 2
            return f"area = {area:.2f}"
        case Rectangle(width=w, height=h) if w == h:
            area = w * h
            return f"area = {area:.2f} (it's a square!)"
        case Rectangle(width=w, height=h):
            area = w * h
            return f"area = {area:.2f}"
        case Triangle(base=b, height=h):
            area = 0.5 * b * h
            return f"area = {area:.2f}"
        case _:
            return "Unknown shape"

# --- Test it ---
shapes = [
    Circle(radius=5),
    Rectangle(width=4, height=6),
    Triangle(base=3, height=8),
    Circle(radius=0),
    Rectangle(width=10, height=10),
]

for shape in shapes:
    label = str(shape).replace("(", "(").replace(")", ")")
    match shape:
        case Circle(radius=r):
            tag = f"Circle(r={r})"
        case Rectangle(width=w, height=h):
            tag = f"Rectangle({w} x {h})"
        case Triangle(base=b, height=h):
            tag = f"Triangle(b={b}, h={h})"
        case _:
            tag = str(shape)
    print(f"{tag:19s} -> {compute_area(shape)}")
Solution
def compute_area(shape):
match shape:
case Circle(radius=r):
area = math.pi * r ** 2
return f"area = {area:.2f}"
case Rectangle(width=w, height=h) if w == h:
area = w * h
return f"area = {area:.2f} (it's a square!)"
case Rectangle(width=w, height=h):
area = w * h
return f"area = {area:.2f}"
case Triangle(base=b, height=h):
area = 0.5 * b * h
return f"area = {area:.2f}"
case _:
return "Unknown shape"

Class patterns explained:

  1. case Circle(radius=r): — checks that the subject is a Circle instance AND captures its radius field into r.
  2. This works automatically with @dataclass because dataclasses define __match_args__. You can also write case Circle(r): using positional matching (based on field order).
  3. The class pattern performs an isinstance check under the hood — it will NOT match a Rectangle against Circle(...).

Guard + class pattern combo:

The guarded Rectangle(width=w, height=h) if w == h case comes BEFORE the general Rectangle case. This detects squares. Order matters — the general case would swallow squares if it came first.

Positional vs keyword matching:

# These are equivalent for dataclasses:
case Circle(radius=r): # keyword — explicit, readable
case Circle(r): # positional — uses __match_args__ order

Keyword syntax is preferred in production code because it survives field reordering.

Expected Output
Circle(r=5)         -> area = 78.54
Rectangle(4 x 6)    -> area = 24.00
Triangle(b=3, h=8)  -> area = 12.00
Circle(r=0)         -> area = 0.00
Rectangle(10 x 10)  -> area = 100.00 (it's a square!)
Hints

Hint 1: Class patterns use ClassName(field=pattern) syntax. With dataclasses, you can match on field names directly: case Circle(radius=r):

Hint 2: Combine class patterns with guard clauses for extra checks: case Rectangle(width=w, height=h) if w == h: detects squares.

#9Command Parser with MatchMedium
match-casecommand-patternparsersequence-pattern

Build a command-line parser that handles commands with varying numbers of arguments. Parse strings like "move 10 20" and "help save" using sequence patterns on split words.

Python
def parse_command(raw_input):
    parts = raw_input.strip().split()
    match parts:
        case []:
            return "Error: Empty command"
        case ["quit" | "exit"]:
            return "Action: Quitting application"
        case ["help"]:
            return "Action: Showing help for all commands"
        case ["help", topic]:
            return f"Action: Showing help for '{topic}' command"
        case ["move", x, y] if x.lstrip("-").isdigit() and y.lstrip("-").isdigit():
            return f"Action: Moving to position ({x}, {y})"
        case ["move", _]:
            return "Error: 'move' requires x and y coordinates"
        case ["color", name] if not name.isdigit():
            return f"Action: Setting color to '{name}'"
        case ["color", r, g, b] if all(c.isdigit() for c in (r, g, b)):
            return f"Action: Setting RGB color ({r}, {g}, {b})"
        case ["resize", w, h] if w.isdigit() and h.isdigit():
            return f"Action: Resizing to {w}x{h}"
        case [cmd, *args]:
            return f"Error: Unknown command '{cmd}' with args {args}"

# --- Test it ---
commands = [
    "quit",
    "help",
    "help save",
    "move 10 20",
    "move 5",
    "color red",
    "color 128 64 255",
    "resize 800 600",
    "",
    "dance fast",
]

for cmd in commands:
    print(f"> {cmd}")
    print(f"  {parse_command(cmd)}")
Solution
def parse_command(raw_input):
parts = raw_input.strip().split()
match parts:
case []:
return "Error: Empty command"
case ["quit" | "exit"]:
return "Action: Quitting application"
case ["help"]:
return "Action: Showing help for all commands"
case ["help", topic]:
return f"Action: Showing help for '{topic}' command"
case ["move", x, y] if x.lstrip("-").isdigit() and y.lstrip("-").isdigit():
return f"Action: Moving to position ({x}, {y})"
case ["move", _]:
return "Error: 'move' requires x and y coordinates"
case ["color", name] if not name.isdigit():
return f"Action: Setting color to '{name}'"
case ["color", r, g, b] if all(c.isdigit() for c in (r, g, b)):
return f"Action: Setting RGB color ({r}, {g}, {b})"
case ["resize", w, h] if w.isdigit() and h.isdigit():
return f"Action: Resizing to {w}x{h}"
case [cmd, *args]:
return f"Error: Unknown command '{cmd}' with args {args}"

Pattern techniques used:

  1. OR pattern in sequence: ["quit" | "exit"] matches a single-element list where that element is either "quit" or "exit".
  2. Variable-arity commands: ["help"] matches 0 args, ["help", topic] matches 1 arg. Same command name, different patterns.
  3. Guards for validation: if x.lstrip("-").isdigit() validates that arguments are numeric before binding.
  4. Star pattern as catch-all: [cmd, *args] captures the command name and any number of arguments.

Real-world applications:

This pattern is extremely common in CLI tools, chatbots, game engines, and REPL interfaces. The match/case approach replaces complex if/elif chains with clear structural declarations of what each command looks like.

Error handling strategy:

Notice how error cases are interspersed with success cases. ["move", _] catches single-argument move commands that fail the 2-argument pattern above it. This "catch near the source" approach gives precise error messages.

Expected Output
> quit
  Action: Quitting application
> help
  Action: Showing help for all commands
> help save
  Action: Showing help for 'save' command
> move 10 20
  Action: Moving to position (10, 20)
> move 5
  Error: 'move' requires x and y coordinates
> color red
  Action: Setting color to 'red'
> color 128 64 255
  Action: Setting RGB color (128, 64, 255)
> resize 800 600
  Action: Resizing to 800x600
> 
  Error: Empty command
> dance fast
  Error: Unknown command 'dance' with args ['fast']
Hints

Hint 1: Split the command string into a list of words, then match on the resulting list. Different command arities become different sequence patterns.

Hint 2: Use guard clauses to validate arguments: case ["move", x, y] if x.isdigit() and y.isdigit(): ensures numeric arguments.


Hard

#10JSON Schema Validator with MatchHard
match-caserecursive-matchingjson-validationnested-patterns

Build a JSON schema validator that uses match/case to dispatch on schema types. Support string, int (with min/max), list (with item schema), and object (with field schemas) validation — all using recursive pattern matching.

Python
def validate(value, schema):
    """Validate a value against a JSON schema using match/case."""
    match schema:
        case {"type": "string"}:
            if isinstance(value, str):
                return (True, "VALID")
            return (False, f"Expected string, got {type(value).__name__}")

        case {"type": "int", "min": lo, "max": hi}:
            if not isinstance(value, int):
                return (False, f"Expected int, got {type(value).__name__}")
            if value < lo:
                return (False, f"Value {value} < min {lo}")
            if value > hi:
                return (False, f"Value {value} > max {hi}")
            return (True, "VALID")

        case {"type": "int"}:
            if isinstance(value, int):
                return (True, "VALID")
            return (False, f"Expected int, got {type(value).__name__}")

        case {"type": "list", "items": item_schema}:
            if not isinstance(value, list):
                return (False, f"Expected list, got {type(value).__name__}")
            for i, item in enumerate(value):
                ok, msg = validate(item, item_schema)
                if not ok:
                    return (False, f"Item {i}: {msg}")
            return (True, "VALID")

        case {"type": "object", "fields": fields}:
            if not isinstance(value, dict):
                return (False, f"Expected object, got {type(value).__name__}")
            for field_name, field_schema in fields.items():
                if field_name not in value:
                    return (False, f"Missing required field '{field_name}'")
                ok, msg = validate(value[field_name], field_schema)
                if not ok:
                    return (False, f"Field '{field_name}': {msg}")
            return (True, "VALID")

        case _:
            return (False, f"Unknown schema type: {schema}")


# --- Test: string schema ---
string_schema = {"type": "string"}
print(f'Schema: {{"type": "string"}}')
for val in ["hello", 42]:
    ok, msg = validate(val, string_schema)
    label = repr(val)
    print(f"  {label:11s} -> {msg}")

# --- Test: int with range ---
int_schema = {"type": "int", "min": 0, "max": 100}
print(f'\nSchema: {{"type": "int", "min": 0, "max": 100}}')
for val in [42, -5, 150]:
    ok, msg = validate(val, int_schema)
    label = repr(val)
    print(f"  {label:11s} -> {msg}")

# --- Test: list of strings ---
list_schema = {"type": "list", "items": {"type": "string"}}
print(f'\nSchema: {{"type": "list", "items": {{"type": "string"}}}}')
for val in [["a", "b"], ["a", 1]]:
    ok, msg = validate(val, list_schema)
    label = repr(val)
    print(f"  {label:11s} -> {msg}")

# --- Test: nested object ---
user_schema = {
    "type": "object",
    "fields": {
        "name": {"type": "string"},
        "age": {"type": "int", "min": 0, "max": 150},
    }
}
print(f'\nSchema: {{"type": "object", "fields": ...}}')

good_user = {"name": "Alice", "age": 30}
ok, msg = validate(good_user, user_schema)
print(f"  {'Full user':11s} -> {msg}")

bad_user = {"name": "Bob", "age": "thirty"}
ok, msg = validate(bad_user, user_schema)
print(f"  {'Bad user':11s} -> {msg}")
Solution
def validate(value, schema):
match schema:
case {"type": "string"}:
if isinstance(value, str):
return (True, "VALID")
return (False, f"Expected string, got {type(value).__name__}")

case {"type": "int", "min": lo, "max": hi}:
if not isinstance(value, int):
return (False, f"Expected int, got {type(value).__name__}")
if value < lo:
return (False, f"Value {value} < min {lo}")
if value > hi:
return (False, f"Value {value} > max {hi}")
return (True, "VALID")

case {"type": "int"}:
if isinstance(value, int):
return (True, "VALID")
return (False, f"Expected int, got {type(value).__name__}")

case {"type": "list", "items": item_schema}:
if not isinstance(value, list):
return (False, f"Expected list, got {type(value).__name__}")
for i, item in enumerate(value):
ok, msg = validate(item, item_schema)
if not ok:
return (False, f"Item {i}: {msg}")
return (True, "VALID")

case {"type": "object", "fields": fields}:
if not isinstance(value, dict):
return (False, f"Expected object, got {type(value).__name__}")
for field_name, field_schema in fields.items():
if field_name not in value:
return (False, f"Missing required field '{field_name}'")
ok, msg = validate(value[field_name], field_schema)
if not ok:
return (False, f"Field '{field_name}': {msg}")
return (True, "VALID")

case _:
return (False, f"Unknown schema type: {schema}")

Architecture of the recursive validator:

validate({"name": "Alice", "age": 30}, object_schema)
|
+-- validate("Alice", {"type": "string"}) -> VALID
|
+-- validate(30, {"type": "int", "min": 0, "max": 150}) -> VALID

Why match/case excels here:

  1. Mapping patterns naturally dispatch on schema shape. Each schema type has a distinct key structure — match routes to the right handler without any if schema["type"] == ... boilerplate.
  2. Extra keys are ignored. The {"type": "int", "min": lo, "max": hi} pattern matches even if the schema dict has additional keys (like "description"). This makes the validator forward-compatible.
  3. Order matters for specificity. {"type": "int", "min": lo, "max": hi} must come before {"type": "int"} — the latter would match even when min/max are present (because mapping patterns ignore extra keys).

Extending the validator:

You could add "enum" types, "nullable" support, or "oneOf" (union schemas) — each becomes a new case in the match block.

Expected Output
Schema: {"type": "string"}
  "hello"     -> VALID
  42          -> INVALID: Expected string, got int

Schema: {"type": "int", "min": 0, "max": 100}
  42          -> VALID
  -5          -> INVALID: Value -5 < min 0
  150         -> INVALID: Value 150 > max 100

Schema: {"type": "list", "items": {"type": "string"}}
  ['a', 'b']  -> VALID
  ['a', 1]    -> INVALID: Item 1: Expected string, got int

Schema: {"type": "object", "fields": ...}
  Full user   -> VALID
  Bad user    -> INVALID: Field 'age': Expected int, got str
Hints

Hint 1: Use mapping patterns to match the schema dict: case {"type": "string"}: for string validation. Recursive calls handle nested schemas like list items and object fields.

Hint 2: The validator function should call itself recursively: validate(item, item_schema) for each list element and validate(value, field_schema) for each object field.

#11AST Evaluator with Recursive MatchHard
match-caserecursiveasttree-walkinginterpreter

Build a tree-walking interpreter for a simple expression language. Represent expressions as nested tuples (an AST) and use recursive match/case to evaluate them. Support arithmetic, unary negation, variables, and let bindings.

Python
def evaluate(expr, env=None):
    """Evaluate an AST expression using recursive match/case."""
    if env is None:
        env = {}

    match expr:
        # Literal number
        case ("num", value):
            return value

        # Variable lookup
        case ("var", name):
            if name in env:
                return env[name]
            raise NameError(f"Undefined variable: {name}")

        # Binary operations
        case ("add", left, right):
            return evaluate(left, env) + evaluate(right, env)
        case ("sub", left, right):
            return evaluate(left, env) - evaluate(right, env)
        case ("mul", left, right):
            return evaluate(left, env) * evaluate(right, env)
        case ("div", left, right):
            divisor = evaluate(right, env)
            if divisor == 0:
                raise ZeroDivisionError("Division by zero in AST")
            return evaluate(left, env) / divisor

        # Unary negation
        case ("neg", operand):
            return -evaluate(operand, env)

        # Let binding: ("let", name, value_expr, body_expr)
        case ("let", name, value_expr, body_expr):
            value = evaluate(value_expr, env)
            new_env = {**env, name: value}
            return evaluate(body_expr, new_env)

        case _:
            raise ValueError(f"Unknown AST node: {expr}")


def format_expr(expr):
    """Pretty-print an AST expression."""
    match expr:
        case ("num", v):
            return str(v)
        case ("var", name):
            return name
        case ("add", l, r):
            return f"({format_expr(l)} + {format_expr(r)})"
        case ("sub", l, r):
            return f"({format_expr(l)} - {format_expr(r)})"
        case ("mul", l, r):
            return f"({format_expr(l)} * {format_expr(r)})"
        case ("div", l, r):
            return f"({format_expr(l)} / {format_expr(r)})"
        case ("neg", operand):
            return f"(-{format_expr(operand)})"
        case ("let", name, val, body):
            return f"(let {name} = {format_expr(val)} in {format_expr(body)})"
        case _:
            return str(expr)


# --- Test cases ---
expressions = [
    # 2 + 3 = 5
    ("add", ("num", 2), ("num", 3)),

    # (10 - 3) * 2 = 14
    ("mul", ("sub", ("num", 10), ("num", 3)), ("num", 2)),

    # (2 + 3) * (10 / 2) = 25.0
    ("mul",
        ("add", ("num", 2), ("num", 3)),
        ("div", ("num", 10), ("num", 2))),

    # -(4 + 5) = -9
    ("neg", ("add", ("num", 4), ("num", 5))),

    # let x = 10 in (x * 2) + 5 = 25
    ("let", "x", ("num", 10),
        ("add", ("mul", ("var", "x"), ("num", 2)), ("num", 5))),
]

for expr in expressions:
    pretty = format_expr(expr)
    result = evaluate(expr)
    print(f"Expression: {pretty}")
    print(f"  Result: {result}")
    print()
Solution
def evaluate(expr, env=None):
if env is None:
env = {}
match expr:
case ("num", value):
return value
case ("var", name):
if name in env:
return env[name]
raise NameError(f"Undefined variable: {name}")
case ("add", left, right):
return evaluate(left, env) + evaluate(right, env)
case ("sub", left, right):
return evaluate(left, env) - evaluate(right, env)
case ("mul", left, right):
return evaluate(left, env) * evaluate(right, env)
case ("div", left, right):
divisor = evaluate(right, env)
if divisor == 0:
raise ZeroDivisionError("Division by zero")
return evaluate(left, env) / divisor
case ("neg", operand):
return -evaluate(operand, env)
case ("let", name, value_expr, body_expr):
value = evaluate(value_expr, env)
new_env = {**env, name: value}
return evaluate(body_expr, new_env)
case _:
raise ValueError(f"Unknown AST node: {expr}")

This is how real interpreters work.

The pattern match/case on a tagged tuple is the Python equivalent of how interpreters in ML-family languages (Haskell, OCaml, Rust) use pattern matching on algebraic data types to walk an AST.

Key techniques:

  1. Tagged tuples as ADTs: Each node is ("tag", ...fields). The first element identifies the node type, the rest are children. This is a lightweight alternative to a full class hierarchy.
  2. Recursive descent: Each case evaluates its children recursively. The call stack mirrors the AST structure.
  3. Environment threading: The env dict carries variable bindings. let creates a NEW env (via {**env, name: value}) — this gives proper lexical scoping without mutating the parent scope.

Why {**env, name: value} instead of env[name] = value?

Immutable environment update. If a let-body references a variable from an outer scope, mutating env would corrupt the outer scope when the let-body returns. Copying the dict creates a new scope that shadows the outer one.

Extending the interpreter:

# Add comparison operators
case ("eq", left, right):
return evaluate(left, env) == evaluate(right, env)

# Add conditionals
case ("if", cond, then_expr, else_expr):
if evaluate(cond, env):
return evaluate(then_expr, env)
return evaluate(else_expr, env)

# Add function definitions and calls
case ("fn", param, body):
return ("closure", param, body, env)
case ("call", func_expr, arg_expr):
match evaluate(func_expr, env):
case ("closure", param, body, closure_env):
arg = evaluate(arg_expr, env)
return evaluate(body, {**closure_env, param: arg})
Expected Output
Expression: (2 + 3)
  Result: 5

Expression: ((10 - 3) * 2)
  Result: 14

Expression: ((2 + 3) * (10 / 2))
  Result: 25.0

Expression: (-(4 + 5))
  Result: -9

Expression: (let x = 10 in ((x * 2) + 5))
  Result: 25
Hints

Hint 1: Represent the AST as nested tuples: ("add", left, right), ("mul", left, right), ("num", 5). Match on the first element to determine the node type, then recursively evaluate children.

Hint 2: For the "let" binding, pass an environment dict through the evaluator. When you hit ("var", name), look it up in the environment.

#12Protocol Message Router with Nested PatternsHard
match-casenested-patternsprotocolmessage-routingdataclass

Build a protocol message router that dispatches on deeply nested message structures. Combine mapping patterns, literal patterns, capture patterns, guard clauses, and OR patterns to handle authentication, data operations, system commands, and error cases — all in a single match block.

Python
def route_message(msg):
    """Route a protocol message based on its nested structure."""
    match msg:
        # --- Auth messages ---
        case {
            "type": "auth",
            "payload": {"action": "login", "user": user, "token": token}
        }:
            return ("auth", f"Login successful for {user} (token: {token})")

        case {
            "type": "auth",
            "payload": {"action": "login", "user": user, "error": error}
        }:
            return ("error", f"Auth failed for {user}: {error}")

        # --- Data messages ---
        case {
            "type": "data",
            "payload": {"action": "query", "table": table, "sql": sql},
            "meta": {"limit": limit}
        }:
            return ("data", f"Query {table}: {sql} (limit: {limit})")

        case {
            "type": "data",
            "payload": {"action": "mutation", "table": table, "sql": sql},
            "meta": {"dry_run": dry}
        }:
            return ("data", f"Mutation {table}: {sql} (dry_run: {dry})")

        # --- System messages ---
        case {
            "type": "system",
            "payload": {"action": "heartbeat" | "ping", "source": src},
            "meta": {"timestamp": ts, "latency_ms": lat}
        }:
            return ("system", f"Heartbeat from {src} at {ts} (latency: {lat}ms)")

        case {
            "type": "system",
            "payload": {
                "action": "shutdown",
                "requested_by": user,
                "graceful": graceful
            },
            "meta": {"timeout": timeout}
        } if isinstance(timeout, int) and timeout > 0:
            return ("system",
                    f"Shutdown requested by {user} "
                    f"(graceful: {graceful}, timeout: {timeout}s)")

        # --- Catch-all ---
        case _:
            return ("error", f"Malformed message: {msg}")


# --- Test messages ---
messages = [
    {
        "type": "auth",
        "payload": {"action": "login", "user": "admin", "token": "tok_abc123"},
    },
    {
        "type": "data",
        "payload": {
            "action": "query",
            "table": "users",
            "sql": "SELECT * FROM users WHERE active=1"
        },
        "meta": {"limit": 50, "request_id": "req_001"},
    },
    {
        "type": "data",
        "payload": {
            "action": "mutation",
            "table": "users",
            "sql": "INSERT INTO users VALUES ('alice', 30)",
        },
        "meta": {"dry_run": False, "request_id": "req_002"},
    },
    {
        "type": "system",
        "payload": {"action": "heartbeat", "source": "worker-01"},
        "meta": {"timestamp": 1700000000, "latency_ms": 45},
    },
    {
        "type": "system",
        "payload": {
            "action": "shutdown",
            "requested_by": "admin",
            "graceful": True,
        },
        "meta": {"timeout": 30},
    },
    {
        "type": "auth",
        "payload": {
            "action": "login",
            "user": "unknown_user",
            "error": "INVALID_CREDENTIALS"
        },
    },
    {"action": "dance"},
]

# --- Process and track ---
print("--- Processing messages ---")
route_counts = {}
for msg in messages:
    category, description = route_message(msg)
    route_counts[category] = route_counts.get(category, 0) + 1
    print(f"[{category.upper():8s}] {description}")

print("\n--- Route summary ---")
for cat, count in sorted(route_counts.items()):
    print(f"{cat:9s}: {count} messages")
Solution
def route_message(msg):
match msg:
case {"type": "auth", "payload": {"action": "login", "user": user, "token": token}}:
return ("auth", f"Login successful for {user} (token: {token})")
case {"type": "auth", "payload": {"action": "login", "user": user, "error": error}}:
return ("error", f"Auth failed for {user}: {error}")
case {"type": "data", "payload": {"action": "query", "table": table, "sql": sql}, "meta": {"limit": limit}}:
return ("data", f"Query {table}: {sql} (limit: {limit})")
case {"type": "data", "payload": {"action": "mutation", "table": table, "sql": sql}, "meta": {"dry_run": dry}}:
return ("data", f"Mutation {table}: {sql} (dry_run: {dry})")
case {"type": "system", "payload": {"action": "heartbeat" | "ping", "source": src}, "meta": {"timestamp": ts, "latency_ms": lat}}:
return ("system", f"Heartbeat from {src} at {ts} (latency: {lat}ms)")
case {"type": "system", "payload": {"action": "shutdown", "requested_by": user, "graceful": graceful}, "meta": {"timeout": timeout}} if isinstance(timeout, int) and timeout > 0:
return ("system", f"Shutdown requested by {user} (graceful: {graceful}, timeout: {timeout}s)")
case _:
return ("error", f"Malformed message: {msg}")

Every pattern feature used in one function:

FeatureExample in this problem
Mapping pattern{"type": "auth", "payload": ...}
Nested mapping"payload": {"action": "login", "user": user}
Literal pattern"auth", "login", "query"
Capture patternuser, token, table, sql
OR pattern"heartbeat" | "ping"
Guard clauseif isinstance(timeout, int) and timeout > 0
Wildcard defaultcase _:

Why this pattern works in production:

  1. Self-documenting: Each case IS the protocol spec. Reading the match block tells you exactly what message shapes the system accepts.
  2. Fail-safe: The wildcard catches malformed messages instead of crashing with a KeyError.
  3. Extra keys ignored: The auth messages do not have a "meta" key, and that is fine — mapping patterns only require the keys they specify.
  4. Disambiguation by shape: Auth success has "token", auth failure has "error". The same top-level type routes to different handlers based on nested structure.

This is the pattern you would use for:

  • WebSocket message handling
  • gRPC/protobuf dispatch (after deserialization)
  • Event-driven architectures (Kafka/RabbitMQ consumers)
  • API gateway routing based on request shape
  • Game networking (different packet types)

Performance note: Python evaluates cases top to bottom. Put the most frequent message types first for best average-case performance in hot loops.

Expected Output
--- Processing messages ---
[AUTH    ] Login successful for admin (token: tok_abc123)
[DATA    ] Query users: SELECT * FROM users WHERE active=1 (limit: 50)
[DATA    ] Mutation users: INSERT INTO users VALUES ('alice', 30) (dry_run: False)
[SYSTEM  ] Heartbeat from worker-01 at 1700000000 (latency: 45ms)
[SYSTEM  ] Shutdown requested by admin (graceful: True, timeout: 30s)
[ERROR   ] Auth failed for unknown_user: INVALID_CREDENTIALS
[ERROR   ] Malformed message: {'action': 'dance'}

--- Route summary ---
auth     : 2 messages
data     : 2 messages
system   : 2 messages
error    : 1 messages
Hints

Hint 1: Nest mapping patterns inside mapping patterns: case {"type": "data", "payload": {"action": "query", "table": t}}: matches a specific nested structure in one pattern.

Hint 2: Use the | OR pattern inside nested structures: case {"type": "system", "payload": {"action": "heartbeat" | "ping"}}: matches multiple actions in one case.

© 2026 EngineersOfAI. All rights reserved.