Pattern Matching in Python

Overview of Pattern Matching

Python introduced pattern matching in PEP 634 starting with Python 3.10, allowing a cleaner and more expressive way to match data structures. It is similar to switch-case statements in other languages, but more powerful because it can deconstruct complex data structures.

Pattern matching works with a new statement: match and case. You can use it to match values against patterns, deconstruct data, and bind variables. Let's go over some examples:

Basic Syntax:

match <expression>:
    case <pattern>:
        # action
    case <pattern>:
        # action

Example 1: Simple Value Matching

def describe_number(num):
    match num:
        case 0:
            return "zero"
        case 1:
            return "one"
        case _:
            return "some number"

print(describe_number(0))  # Output: zero
print(describe_number(2))  # Output: some number
  • Here, _ is a wildcard pattern that matches anything. It's like default in switch-case statements.

Example 2: Matching Data Structures

You can match lists, tuples, and other structures using patterns.

def describe_list(lst):
    match lst:
        case []:
            return "empty list"
        case [x]:
            return f"list with one element: {x}"
        case [x, y]:
            return f"list with two elements: {x}, {y}"
        case [x, y, *rest]:
            return f"list with at least two elements: {x}, {y} and others: {rest}"

print(describe_list([]))              # Output: empty list
print(describe_list([1]))             # Output: list with one element: 1
print(describe_list([1, 2]))          # Output: list with two elements: 1, 2
print(describe_list([1, 2, 3, 4]))    # Output: list with at least two elements: 1, 2 and others: [3, 4]

Example 3: Matching Tuples

You can match tuples and bind variables from them:

def describe_tuple(tup):
    match tup:
        case (x, y):
            return f"two elements: {x}, {y}"
        case (x, y, z):
            return f"three elements: {x}, {y}, {z}"
        case _:
            return "other"

print(describe_tuple((1, 2)))         # Output: two elements: 1, 2
print(describe_tuple((1, 2, 3)))      # Output: three elements: 1, 2, 3

Example 4: Matching Custom Classes

Pattern matching can also deconstruct custom classes by matching attributes.

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

def describe_point(pt):
    match pt:
        case Point(x, y):
            return f"Point at ({x}, {y})"

p = Point(3, 4)
print(describe_point(p))  # Output: Point at (3, 4)

Example 5: Guarding with if

You can add conditions (guards) to patterns.

def check_number(n):
    match n:
        case x if x > 0:
            return "positive"
        case x if x < 0:
            return "negative"
        case _:
            return "zero"

print(check_number(10))   # Output: positive
print(check_number(-5))   # Output: negative
print(check_number(0))    # Output: zero

Example 6: Nested Matching

You can match nested structures like lists of tuples or dictionaries.

def describe_data(data):
    match data:
        case {"name": name, "age": age}:
            return f"Name: {name}, Age: {age}"
        case _:
            return "Unknown format"

data = {"name": "Alice", "age": 30}
print(describe_data(data))  # Output: Name: Alice, Age: 30

Summary of Key Concepts:

  1. Literal Matching: Match exact values.

  2. Variable Binding: Bind parts of the structure to variables.

  3. Wildcard _: Match anything (similar to default in switch).

  4. Guards (if): Add conditions to patterns.

  5. Deconstruction: Unpack data structures (like lists, tuples, and classes).

More Advanced Examples

Here are some advanced examples of Python pattern matching that demonstrate how to leverage its full power, including deep nesting, combining patterns, and using classes with custom match behavior.

1. Nested Pattern Matching

Pattern matching can handle deeply nested structures, making it easier to deconstruct and extract data from them.

def describe_nested_list(lst):
    match lst:
        case [a, [b, c]]:
            return f"Outer element: {a}, Nested elements: {b}, {c}"
        case [a, [b, [c, d]]]:
            return f"Deeply nested: {a}, {b}, {c}, {d}"
        case _:
            return "Unknown format"

print(describe_nested_list([1, [2, 3]]))            # Output: Outer element: 1, Nested elements: 2, 3
print(describe_nested_list([1, [2, [3, 4]]]))       # Output: Deeply nested: 1, 2, 3, 4

2. Using OR (|) Pattern

You can use the | operator to match multiple possible patterns in one case.

def describe_value(val):
    match val:
        case 1 | 2 | 3:
            return "One, Two, or Three"
        case "hello" | "world":
            return "A greeting"
        case _:
            return "Something else"

print(describe_value(1))          # Output: One, Two, or Three
print(describe_value("hello"))    # Output: A greeting
print(describe_value(10))         # Output: Something else

3. Matching with Sequences and Wildcards

Here, we handle both the sequence length and use wildcards (_) to match parts of the sequence while ignoring others.

def process_sequence(seq):
    match seq:
        case [first, second, *rest]:
            return f"First two: {first}, {second}, and the rest: {rest}"
        case [_, _]:
            return "Two elements, but we don’t care what they are"
        case _:
            return "Other sequence"

print(process_sequence([1, 2, 3, 4, 5]))   # Output: First two: 1, 2, and the rest: [3, 4, 5]
print(process_sequence([9, 8]))            # Output: Two elements, but we don’t care what they are

4. Combining Patterns in Lists

Pattern matching supports combining patterns for specific parts of a list. In the example below, you can handle lists with specific conditions while also managing complex data structures.

def match_complex_list(lst):
    match lst:
        case [first, [*middle], last]:
            return f"First: {first}, Middle: {middle}, Last: {last}"
        case [first, *_]:
            return f"Starts with {first}, rest unknown"
        case _:
            return "Other"

print(match_complex_list([1, [2, 3, 4], 5]))   # Output: First: 1, Middle: [2, 3, 4], Last: 5
print(match_complex_list([10, 20, 30, 40]))    # Output: Starts with 10, rest unknown

5. Matching Custom Classes with Guards

In more advanced scenarios, you may need to add guards (conditions) to your pattern matching. Here, we match a Point class, but only if certain conditions are met.

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

def describe_point_with_guards(pt):
    match pt:
        case Point(x, y) if x == y:
            return f"Point on the diagonal: ({x}, {y})"
        case Point(x, y) if x > 0 and y > 0:
            return f"Point in the first quadrant: ({x}, {y})"
        case Point(x, y):
            return f"Point at ({x}, {y})"

p1 = Point(3, 3)
p2 = Point(5, 6)
p3 = Point(-1, -2)

print(describe_point_with_guards(p1))   # Output: Point on the diagonal: (3, 3)
print(describe_point_with_guards(p2))   # Output: Point in the first quadrant: (5, 6)
print(describe_point_with_guards(p3))   # Output: Point at (-1, -2)

6. Custom __match_args__ in Classes

Python allows you to customize how your classes work with pattern matching by defining the __match_args__ attribute. This tells Python which attributes to use for matching.

class Rectangle:
    __match_args__ = ("width", "height")
    
    def __init__(self, width, height):
        self.width = width
        self.height = height

def describe_shape(shape):
    match shape:
        case Rectangle(10, 10):
            return "It's a square"
        case Rectangle(width, height):
            return f"Rectangle of width {width} and height {height}"

r1 = Rectangle(10, 10)
r2 = Rectangle(20, 30)

print(describe_shape(r1))   # Output: It's a square
print(describe_shape(r2))   # Output: Rectangle of width 20 and height 30

7. Matching with Enums

You can also match against enums, which is helpful when you have a predefined set of values.

from enum import Enum

class Color(Enum):
    RED = 1
    GREEN = 2
    BLUE = 3

def describe_color(color):
    match color:
        case Color.RED:
            return "Color is red"
        case Color.GREEN:
            return "Color is green"
        case Color.BLUE:
            return "Color is blue"

print(describe_color(Color.RED))    # Output: Color is red

8. Matching Dictionary-Like Structures

Pattern matching can also work with dictionaries. Here's an example where you match specific keys and deconstruct their values.

def describe_dict(d):
    match d:
        case {"name": name, "age": age}:
            return f"Person's name is {name} and they are {age} years old"
        case {"name": name}:
            return f"Person's name is {name}"
        case _:
            return "Unknown structure"

data1 = {"name": "Alice", "age": 30}
data2 = {"name": "Bob"}
data3 = {"age": 25}

print(describe_dict(data1))    # Output: Person's name is Alice and they are 30 years old
print(describe_dict(data2))    # Output: Person's name is Bob
print(describe_dict(data3))    # Output: Unknown structure

9. Combining Lists and Guards

In this example, we handle both pattern matching with list lengths and apply guards to handle specific conditions.

def process_numbers(numbers):
    match numbers:
        case [x, y] if x + y == 10:
            return "Two numbers sum to 10"
        case [x, y, z] if x * y * z == 30:
            return "Three numbers product is 30"
        case _:
            return "Unknown pattern"

print(process_numbers([4, 6]))          # Output: Two numbers sum to 10
print(process_numbers([2, 3, 5]))       # Output: Three numbers product is 30
print(process_numbers([1, 2]))          # Output: Unknown pattern

Summary of Advanced Concepts:

  1. Nested patterns: Handle deep nesting within structures.

  2. OR patterns (|): Match multiple alternatives in one case.

  3. Sequences and wildcards: Match lists or tuples of arbitrary length and ignore parts.

  4. Guards (if): Add additional logic to conditions.

  5. Custom classes: Use __match_args__ to control how classes match.

  6. Enums and dictionaries: Easily match common Python data structures.

Last updated

Was this helpful?