How to Use __match_args__ in Python Pattern Matching

Python 3.10 introduced structural pattern matching via the match and case statements, allowing developers to inspect and unpack complex data structures cleanly. While custom classes automatically support keyword-based pattern matching, they do not support positional pattern matching out of the box. The __match_args__ class attribute bridges this gap by defining a tuple of attribute names that map positional parameters in a pattern directly to an instance's attributes.

The Problem: Keyword vs. Positional Matching

When matching instances of user-defined classes, Python naturally expects keyword patterns. For example:

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

def check_point(pt):
    match pt:
        case Point(x=0, y=0):  # Works by default
            print("Origin")
        case Point(0, 0):      # Raises TypeError: Point() accepts 0 positional sub-patterns
            print("Origin")

Attempting to match Point(0, 0) without additional configuration raises a TypeError. Python does not inherently know whether the first positional argument corresponds to x or y.

The Role of __match_args__

__match_args__ is a class-level tuple of strings that specifies the exact sequence of attributes to bind to positional patterns. When Python encounters a pattern with positional arguments, it reads __match_args__ to translate those positional slots into keyword attribute lookups.

By defining __match_args__, you make positional matching work seamlessly:

class Point:
    __match_args__ = ("x", "y")

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

def evaluate(pt):
    match pt:
        case Point(0, 0):
            print("Origin")
        case Point(0, y):
            print(f"On the Y-axis at {y}")
        case Point(x, 0):
            print(f"On the X-axis at {x}")
        case Point(x, y):
            print(f"Point at ({x}, {y})")

When evaluating case Point(0, y), Python reads the first element of __match_args__ ("x") and matches 0 against pt.x. It then reads the second element ("y") and assigns pt.y to the variable y.

Automatic Support in Standard Library Classes

You do not always need to define __match_args__ manually:

If you are using plain classes, explicit assignment of __match_args__ is required.

Partial Matching and Inheritance

The number of positional sub-patterns in a case statement does not need to equal the total length of __match_args__. Positional arguments are matched from left to right:

class Person:
    __match_args__ = ("name", "age", "role")

    def __init__(self, name, age, role):
        self.name = name
        self.age = age
        self.role = role

# Matches only the first attribute ("name") positionally
match person:
    case Person("Alice"):
        print("Hello Alice")

Subclasses inherit __match_args__ from their parent class unless explicitly overridden. When creating a subclass that introduces new attributes, redefine __match_args__ to include both parent and child attributes if you intend to expose the new fields to positional matching.