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 likedefault
inswitch-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
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:
Literal Matching: Match exact values.
Variable Binding: Bind parts of the structure to variables.
Wildcard
_
: Match anything (similar todefault
in switch).Guards (
if
): Add conditions to patterns.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
|
) PatternYou 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
__match_args__
in ClassesPython 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:
Nested patterns: Handle deep nesting within structures.
OR patterns (
|
): Match multiple alternatives in one case.Sequences and wildcards: Match lists or tuples of arbitrary length and ignore parts.
Guards (
if
): Add additional logic to conditions.Custom classes: Use
__match_args__
to control how classes match.Enums and dictionaries: Easily match common Python data structures.
Last updated
Was this helpful?