Python Match

Python's match Statement: Structural Pattern Matching (Python 3.10+)

The match statement allows you to compare a subject (an expression) against several patterns and execute a block of code when a pattern matches. It's a more declarative and often more readable alternative to long if-elif-else chains, particularly when your conditions involve inspecting the structure or shape of data.

It's inspired by pattern matching features found in functional programming languages.



1. Basic match Statement:

The simplest form involves matching a single value against literal patterns.

Syntax :

match subject:
    case pattern1:
        # Code to execute if subject matches pattern1
    case pattern2:
        # Code to execute if subject matches pattern2
    case _: # The "wildcard" pattern (like 'else')
        # Code to execute if no other pattern matches


Key Points :


Example 1.1: Matching a string (commands) :

command = "start"
match command:
    case "start":
        print("System is starting...")
    case "stop":
        print("System is shutting down.")
    case "restart":
        print("System is restarting now.")
    case _: # Default case if no match found
        print(f"Unknown command: '{command}'")
# Another example:
command = "status"
match command:
    case "start":
        print("System is starting...")
    case "stop":
        print("System is shutting down.")
    case "restart":
        print("System is restarting now.")
    case _:
        print(f"Unknown command: '{command}'")

Explanation :

The command variable is the subject. Each case checks if command exactly matches the string literal ("start", "stop", "restart"). If command is "status", it doesn't match any of the explicit cases, so the wildcard _ case is executed.



2. Matching Multiple Literals with | (OR operator) :

You can match against multiple literal values using the | (OR) operator within a single case.


Example 2.1: Matching multiple HTTP status codes :

status_code = 403
match status_code:
    case 200:
        print("OK: Request successful.")
    case 400 | 401 | 403 | 404: # Matches any of these client errors
        print("Client Error: Please check your request or authentication.")
    case 500 | 502 | 503: # Matches any of these server errors
        print("Server Error: Something went wrong on the server side.")
    case _:
        print(f"Unhandled status code: {status_code}")

Explanation :

If status_code is 403, it matches 400 | 401 | 403 | 404 and the corresponding message is printed.



3. Capturing Patterns (Variable Patterns) :

You can capture parts of the subject into variables for later use within the case block. This is done by using a variable name as a pattern.


Example 3.1: Capturing a simple value :

action = "download"
match action:
    case "upload":
        print("Initiating file upload.")
    case "download" as file_action: # Captures "download" into file_action
        print(f"Initiating file {file_action}.")
    case cmd: # Captures any other value into 'cmd'
        print(f"Executing generic command: {cmd}")
# Another example:
action = "process"
match action:
    case "upload":
        print("Initiating file upload.")
    case "download" as file_action:
        print(f"Initiating file {file_action}.")
    case cmd: # 'process' is captured by 'cmd'
        print(f"Executing generic command: {cmd}")

Explanation :

When action is "download", it matches case "download" as file_action:, and the string "download" is assigned to the file_action variable. If action is "process", it doesn't match the specific literals, so it falls to case cmd:, and "process" is assigned to cmd.



4. Matching Sequence Patterns (Lists and Tuples) :

You can match against the structure and elements of sequences like lists and tuples.



Key Points:



Example 4.1: Matching coordinates :

point1 = (1, 2)
point2 = [5, 10, 15]
point3 = (0, 0)
point4 = (3, -1)
for point in [point1, point2, point3, point4]:
    match point:
        case (0, 0):
            print(f"{point}: Origin.")
        case (x, y): # Matches any tuple of two elements, captures them into x, y
            print(f"{point}: Point at ({x}, {y}).")
        case [x, y, z]: # Matches a list of three elements
            print(f"{point}: 3D point at [{x}, {y}, {z}].")
        case _:
            print(f"{point}: Unrecognized point format.")

Explanation :



Example 4.2: Using the star pattern * (variable-length sequences)

data_entry1 = ["log", "info", "User logged in."]
data_entry2 = ["error", "Permission denied.", "main.py", 102]
data_entry3 = ["event", "App startup"]
for entry in [data_entry1, data_entry2, data_entry3]:
    match entry:
        case ["log", level, message]: # Matches a 3-element list with specific first element
            print(f"LOG ({level.upper()}): {message}")
        case ["error", message, *details]: # Matches "error", captures message, and remaining elements into 'details' list
            print(f"ERROR: {message} (Details: {details})")
        case ["event", *args]: # Matches "event" and captures remaining elements into 'args'
            print(f"EVENT: {args}")
        case _:
            print(f"Unknown data entry: {entry}")

Explanation :



5. Matching Mapping Patterns (Dictionaries):

You can match against the structure and keys/values of dictionaries.


Key Points:



Example 5.1: Matching user profiles :

user1 = {"name": "Alice", "age": 30, "city": "New York"}
user2 = {"name": "Bob", "role": "admin"}
user3 = {"username": "charlie_dev"}
user4 = {"name": "David", "age": 17, "status": "active"}
for user in [user1, user2, user3, user4]:
    match user:
        case {"name": name, "age": age} if age >= 18: # Matches if 'name' and 'age' keys exist, and age is >= 18
            print(f"Adult User: {name}, Age: {age}")
        case {"name": name, "age": age}: # Matches if 'name' and 'age' keys exist (any age)
            print(f"Minor User: {name}, Age: {age}")
        case {"name": name, "role": role}: # Matches if 'name' and 'role' keys exist
            print(f"User with Role: {name}, Role: {role}")
        case {"name": name, **other_info}: # Matches if 'name' exists, captures rest into other_info dict
            print(f"User (Name only): {name}, Other info: {other_info}")
        case _:
            print(f"Unknown user format: {user}")

Explanation :



6. Guards (if clauses in case):

You can add an if clause (a "guard") to a case pattern. The case only matches if both the pattern matches and the if condition is true.

Syntax:

case pattern if condition:
    # Code to execute if pattern matches AND condition is True

Key Points:



Example 6.1: Filtering numbers

num = 7
match num:
    case n if n % 2 == 0:
        print(f"{n} is an even number.")
    case n if n % 2 != 0:
        print(f"{n} is an odd number.")
    case _:
        print("Not a number?")


Example 6.2: Combining patterns and guards (from user profile example)

user = {"name": "Charlie", "age": 15, "city": "London"}
match user:
    case {"name": name, "age": age} if age >= 18:
        print(f"Adult user {name} ({age}).")
    case {"name": name, "age": age} if age < 18:
        print(f"Minor user {name} ({age}).")
    case {"name": name, **details}:
        print(f"User {name} with other details: {details}")
    case _:
        print("Invalid user data.")

Explanation :

The if age >= 18 acts as a guard. The case will only activate if user has name and age keys and the value of age is 18 or greater.



7. Class Patterns (Matching Objects):

This is one of the most powerful features. You can match against instances of classes and extract attributes.

Syntax

case ClassName(attribute1=var1, attribute2=var2, ...):
    # Code to execute if subject is an instance of ClassName
    # and attributes match/are captured

Key Points:



Example 7.1: Matching geometric shapes

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    # Optional: Define __match_args__ for positional matching (more advanced)
    def __match_args__(self):
        return ("x", "y")

class Circle:
    def __init__(self, center, radius):
        self.center = center # center is a Point object
        self.radius = radius
    def __match_args__(self):
        return ("center", "radius") # Allows matching like Circle(Point(x,y), r)

class Rectangle:
    def __init__(self, top_left, bottom_right):
        self.top_left = top_left
        self.bottom_right = bottom_right

# Create some shape instances
p1 = Point(10, 20)
p_origin = Point(0, 0)
c1 = Circle(Point(5, 5), 10)
r1 = Rectangle(Point(0, 10), Point(20, 0))
shapes = [p1, p_origin, c1, r1, "not a shape"]

for shape in shapes:
    match shape:
        case Point(0, 0): # Matches Point object where x is 0 and y is 0
            print(f"Shape: Origin Point at ({shape.x}, {shape.y})")
        case Point(x=x_coord, y=y_coord): # Matches any Point object, captures x and y
            print(f"Shape: Generic Point at ({x_coord}, {y_coord})")
        case Circle(center=Point(cx, cy), radius=r): # Nested pattern: Circle with a Point center
            print(f"Shape: Circle centered at ({cx}, {cy}) with radius {r}")
        case Rectangle(top_left=Point(tx, ty), bottom_right=Point(bx, by)):
            print(f"Shape: Rectangle from ({tx}, {ty}) to ({bx}, {by})")
        case _:
            print(f"Shape: Unknown/Unmatched ({type(shape)})")

Explanation :



8. as keyword for Alias (Subpatterns) :

You can use the as keyword to give an alias to a subpattern, allowing you to refer to the matched part of the subject directly.


Example 8.1: Capturing the entire matched object or sub-structure

data_item1 = ["command", "start"]
data_item2 = ("log", "info", "User logged in.")
data_item3 = {"type": "user", "id": 123}

for item in [data_item1, data_item2, data_item3]:
    match item:
        case ["command", action] as full_command: # Captures 'action' and the entire list as 'full_command'
            print(f"Detected command: {action} (Full: {full_command})")
        case ("log", level, _) as log_entry if level == "error": # Captures level, ignores third, and captures full tuple
            print(f"Critical Log Entry: {log_entry}")
        case {"type": "user", "id": user_id} as user_obj: # Captures user_id and the entire dict as user_obj
            print(f"Matched User object: ID {user_id}, Object: {user_obj}")
        case _:
            print(f"Unmatched item: {item}")

Explanation :

In case ["command", action] as full_command:, action gets the second element, and full_command gets the entire list ["command", "start"]. This is useful when you need to inspect individual parts but also operate on the whole matched structure.



9. Underscore _ as a Wildcard or Ignored Value :

The underscore _ has two main uses:


Example 9.1: Ignoring values in sequence patterns

point_3d = (10, 20, 30)
point_2d = (5, 8)
invalid_point = (1, 2, 3, 4)

for p in [point_3d, point_2d, invalid_point]:
    match p:
        case (x, y, _): # Matches a 3-element tuple, captures first two, ignores third
            print(f"3D point (first two coords): ({x}, {y})")
        case (x, _): # Matches a 2-element tuple, captures first, ignores second
            print(f"2D point (first coord): ({x})")
        case _:
            print(f"Not a recognized point: {p}")

Explanation :

case (x, y, _) matches (10, 20, 30), captures 10 to x, 20 to y, and discards 30.



Advantages of match-case over if-elif-else :

  1. Readability: Especially for complex, nested data structures, match makes the code much cleaner and easier to understand by explicitly showing the expected structure.
  2. Exhaustiveness Checks (potential future tooling): While not a built-in feature of Python's runtime, pattern matching in other languages often allows static analysis tools to warn you if you haven't covered all possible cases, which could be a future benefit for Python.
  3. Conciseness: It can significantly reduce boilerplate code compared to multiple if and and conditions.
  4. No Fall-through: Unlike some other languages (like C's switch), Python's match does not fall through. Only the first matching case is executed.
  5. Direct Deconstruction: It directly deconstructs values (like extracting elements from a list or attributes from an object) into variables, which reduces manual assignments.


When to use match-case?



When if-elif-else might still be better?



Here are 5 simple and easy-to-understand examples of the match statement in Python:

Note: These examples require Python 3.10 or higher.



Example 1: Match a String

day = "Sunday"
match day:
    case "Monday":
        print("Start of the week")
    case "Sunday":
        print("Relax! It's weekend.")
    case _:
        print("Just a regular day")

#Output- Relax! It's weekend.


Example 2: Match a Number

number = 5

match number:
    case 1:
        print("One")
    case 5:
        print("Five")
    case _:
        print("Unknown number")
#Output- Five


Example 3: Match with Multiple Values

fruit = "apple"
match fruit:
    case "apple" | "banana" | "mango":
        print("It's a fruit we love!")
    case _:
        print("It's some other fruit.")
#Output- It's a fruit we love!


Example 4: Match a Tuple

point = (0, 5)

match point:
    case (0, 0):
        print("Origin")
    case (0, y):
        print(f"Y-axis at y = {y}")
    case (x, 0):
        print(f"X-axis at x = {x}")
    case (x, y):
        print(f"Point is at ({x}, {y})")
#Output- Y-axis at y = 5


Example 5: Match with Condition (Guard)

age = 17

match age:
    case x if x < 18:
        print("You are a minor.")
    case x if x >= 18:
        print("You are an adult.")
#Output- You are a minor.

Conclusion :

The match statement is a powerful addition to Python's control flow mechanisms. It encourages a more declarative style of programming when dealing with varied data structures and provides a cleaner, more readable way to express complex conditional logic. While it takes some getting used to, mastering match will undoubtedly make your Python code more elegant and maintainable for many real-world scenarios.