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:
- Dataclasses: Decorating a class with
@dataclasses.dataclassautomatically generates__match_args__based on the order in which fields are defined. - Named Tuples: Classes created via
collections.namedtupleortyping.NamedTuplealso generate__match_args__automatically.
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.