Python Class Pattern Matching: Syntax and Practical Use
python class pattern matching: Learn how to use Python class pattern matching with match statements, including positional and keyword patterns, __match_args__, and com...
Python class pattern matching, introduced in Python 3.10 as part of structural pattern matching, lets you match objects against class structures directly inside a match statement. Instead of manually checking attributes with if statements, you can write a pattern that mirrors the class layout and have Python extract the fields you need. This keeps dispatch logic readable and reduces the amount of boilerplate around type checks and attribute access.
The match Statement and Class Patterns
The match statement evaluates an expression and compares it against a sequence of patterns. A class pattern looks like a constructor call, but it does not call the class; it checks whether the subject is an instance of that class and then binds variables to attributes. The simplest form matches an object's type:
class Point: def __init__(self, x, y): self.x = x self.y = y def describe(point): match point: case Point(): print("It's a Point") case _: print("Unknown")
Here case Point() matches any instance of Point, regardless of its attribute values. The parentheses are required; case Point: would be a capture pattern that binds the entire object to the name Point, which is almost never what you want.
Matching Against Class Attributes with Keyword Patterns
To inspect or extract attributes, use keyword patterns inside the parentheses. Each keyword corresponds to an attribute name on the subject:
def locate(point): match point: case Point(x=0, y=0): print("Origin") case Point(x=x_val, y=y_val): print(f"Point at ({x_val}, {y_val})") case _: print("Not a Point")
The first pattern requires both x and y to be exactly 0. The second binds x_val and y_val to the object's x and y attributes. Keyword patterns are evaluated using equality for literal values, so x=0 works only if point.x == 0. This is a common source of confusion: the pattern uses ==, not identity.
You can mix literal values and capture variables in the same pattern. For example, case Point(x=0, y=y_val) matches any point on the y-axis and binds the y coordinate.
Using Positional Patterns with match_args
Class patterns also support positional arguments, but only if the class defines __match_args__. This attribute is a tuple of attribute names in the order they should be matched positionally. Without it, Python raises a TypeError at runtime when you try to use positional patterns.
class Point: __match_args__ = ("x", "y") def __init__(self, x, y): self.x = x self.y = y def describe(point): match point: case Point(0, 0): print("Origin") case Point(x, y): print(f"({x}, {y})")
Here Point(0, 0) is equivalent to Point(x=0, y=0), and Point(x, y) binds x and y positionally. Defining __match_args__ makes patterns more concise when the attribute order is stable and obvious. It also makes the class easier to use with pattern matching in libraries that expose data objects.
Combining Class Patterns with Guards and Subpatterns
Class patterns become more powerful when combined with guards and nested patterns. A guard is an if clause that adds an arbitrary condition after the pattern. It runs only after the pattern matches, and it can reference bound variables.
def quadrant(point): match point: case Point(x, y) if x > 0 and y > 0: print("Quadrant I") case Point(x, y) if x < 0 and y > 0: print("Quadrant II") case Point(x, y) if x < 0 and y < 0: print("Quadrant III") case Point(x, y) if x > 0 and y < 0: print("Quadrant IV") case Point(x, y): print("On an axis")
Nested patterns let you match attributes that are themselves objects. For instance, if a Line has a start and end point, you can destructure both in one pattern:
class Line: __match_args__ = ("start", "end") def __init__(self, start, end): self.start = start self.end = end def is_horizontal(line): match line: case Line(Point(x1, y1), Point(x2, y2)) if y1 == y2: return True case _: return False
The pattern Line(Point(x1, y1), Point(x2, y2)) checks that line is a Line, that its start and end are Point instances, and binds all four coordinates. This reduces nested attribute access to a single declarative statement.
Common Pitfalls and Runtime Behavior
Several details can trip up developers new to class pattern matching.
First, class patterns only match instances of the exact class, not subclasses. If you have a subclass ColoredPoint(Point), a case Point() will not match it. To match subclasses, you need to use a wildcard or a capture pattern and then check the type manually, or you can use a class pattern with a guard that checks isinstance.
Second, attribute access in patterns uses getattr internally. If the class defines properties or custom __getattr__, those are invoked during matching. This can have side effects or raise exceptions if the attribute is missing. A missing attribute raises AttributeError, which propagates out of the match statement rather than causing the pattern to fail. For example, case Point(z=0) will raise an error if the object has no z attribute.
Third, the order of patterns matters. The first matching pattern wins, so more specific patterns should appear before more general ones. This is especially important when using guards, because a pattern without a guard will match before a guarded pattern even if the guard would fail.
Finally, remember that literal values in patterns use equality, not identity. If you match against an object that defines a custom __eq__, that method is called. This is usually what you want, but it can be surprising when comparing against mutable objects.
Performance and Maintainability Considerations
Class pattern matching is not a performance shortcut. The match statement compiles to a series of isinstance checks and attribute accesses, similar to what you would write manually. The main benefit is readability and the ability to destructure in a single step. For performance-critical code, you should profile to see if pattern matching adds measurable overhead; in most applications it is negligible.
From a maintainability perspective, class patterns make the shape of the data explicit. When a new subclass is introduced, the match statement will not automatically handle it, and you may need to add a new case. This is both a strength and a limitation: it forces you to consider all known variants, but it also means the match statement can become a maintenance point when the class hierarchy grows.
When Class Pattern Matching Is the Right Choice
Class pattern matching is most valuable when you have a closed set of classes and you need to dispatch behavior based on their structure. Typical use cases include parsers, AST visitors, and protocol handlers where each node type has a distinct set of attributes.
Use class patterns when:
- The classes are stable and you control their definitions.
- You need to extract multiple attributes at once.
- The matching logic is easier to read as a pattern than as a chain of if statements.
Avoid class patterns when the class hierarchy is open to extension, when attribute names are dynamic, or when you need to match based on complex relationships between attributes that are better expressed in explicit logic. In those cases, a traditional if/elif chain or a visitor pattern may be more maintainable.
The decision ultimately comes down to whether the pattern mirrors the natural structure of your data. When it does, class pattern matching produces concise, self-documenting code that is easier to extend and debug than equivalent manual checks.