Python Pattern Matching Classes: Using Match with Class Patterns
python pattern matching classes: Learn how to use Python's structural pattern matching with classes: capture attributes, match nested objects, and use guards effectively.
Python's match statement, introduced in 3.10, brings structural pattern matching to the language. When you need to dispatch on the type and shape of an object, class patterns let you match against class instances while extracting attributes in a single step. This article focuses on python pattern matching classes — how to write class patterns, capture values, and avoid common mistakes.
Class Patterns: Matching Type and Structure
A class pattern looks like a constructor call: ClassName(pattern1, pattern2). The pattern matches if the subject is an instance of ClassName and the positional arguments match the corresponding attributes. For example:
class Point: def __init__(self, x, y): self.x = x self.y = y def describe(point): match point: case Point(0, 0): return "Origin" case Point(x, y): return f"Point at ({x}, {y})"
The first case matches only when both x and y are zero. The second captures the coordinates into variables x and y. This works because Point is a class with two positional attributes in its __init__.
Positional patterns rely on the order of attributes as defined in the class. If the class stores attributes in a different order, the pattern will match incorrectly. For clarity, many developers prefer keyword patterns.
Keyword Patterns for Explicit Attribute Matching
Keyword patterns use ClassName(attribute=pattern) to match specific attributes by name. This avoids positional ordering issues and makes the code self-documenting:
match point: case Point(x=0, y=0): return "Origin" case Point(x=x, y=y): return f"Point at ({x}, {y})"
You can mix positional and keyword patterns, but once you use a keyword pattern, all subsequent positional patterns are disallowed. The interpreter matches keyword patterns against the instance's attributes directly, so the class must expose those attributes as instance variables or properties.
Keyword patterns are particularly useful when a class has many attributes and you only care about a few. For example:
class Employee: def __init__(self, name, department, salary): self.name = name self.department = department self.salary = salary match employee: case Employee(department="engineering", salary=salary): print(f"Engineer earns {salary}")
This matches any engineering employee and captures the salary without requiring the other fields.
Capturing Values and Wildcards
A bare variable name in a pattern captures the entire subject. Inside a class pattern, a variable captures the corresponding attribute. The wildcard _ matches anything and discards the value:
match point: case Point(0, _): return "On the y-axis" case Point(_, 0): return "On the x-axis"
Use _ when you need to match a position but don't need the value. Reusing the same variable name twice in one pattern is not allowed; each capture must be unique. If you need to check equality between two attributes, use a guard instead.
Guard Conditions for Additional Checks
Guards are if expressions attached to a case. They run after the pattern matches and can reference captured variables. This lets you enforce relationships that patterns alone cannot express:
match point: case Point(x, y) if x == y: return "On the diagonal" case Point(x, y) if x > y: return "Right of diagonal" case Point(x, y): return "Left of diagonal"
The guard does not affect the pattern's structural match; it only adds a runtime condition. If the guard evaluates to false, the match continues to the next case. Guards are evaluated only after the pattern succeeds, so captured variables are always bound.
Matching Nested Class Structures
Class patterns can be nested. When an attribute is itself an object, you can match its internal structure recursively:
class Rectangle: def __init__(self, top_left, bottom_right): self.top_left = top_left self.bottom_right = bottom_right match rect: case Rectangle(Point(0, 0), Point(x, y)): return f"Rectangle from origin to ({x}, {y})" case Rectangle(Point(x1, y1), Point(x2, y2)): return f"Rectangle spanning ({x1},{y1}) to ({x2},{y2})"
Nested patterns work because each subpattern is applied to the corresponding attribute. This is especially useful when parsing ASTs or handling complex data structures. However, deep nesting can reduce readability. If the pattern becomes too intricate, consider matching on a single attribute and then using a separate match or if-else for the inner structure.
Common Pitfalls and Runtime Behavior
Class patterns only match instances of the exact class, not subclasses. If you have a subclass ColoredPoint(Point), a case Point(x, y) will not match it. To match subclasses, you must match the subclass explicitly or use a wildcard with a guard. This is different from isinstance, which includes subclasses. Keep this in mind when designing class hierarchies.
Another pitfall is relying on __match_args__. By default, positional patterns use the order of arguments in the class's __init__. If you define __match_args__, you can change that order. For example:
class Point: __match_args__ = ("y", "x") def __init__(self, x, y): self.x = x self.y = y
Now Point(0, 1) matches x=1, y=0. This can be confusing, so use it sparingly. Most code is clearer with keyword patterns.
Finally, remember that patterns are matched in order. The first matching case wins. Place more specific patterns before generic ones. A wildcard case _: should always be last to avoid shadowing earlier cases.
Performance and Maintainability Considerations
Class patterns add minimal runtime overhead compared to manual isinstance checks and attribute access. The interpreter performs a type check and attribute extraction in one pass. For most applications, the cost is negligible. However, if you are matching in a hot loop, avoid complex nested patterns that repeatedly access attributes; the overhead is similar to explicit attribute access.
From a maintainability perspective, class patterns centralize type-based dispatch. Instead of a chain of if isinstance(...) blocks, a single match statement expresses all branches clearly. This reduces duplication and makes the code easier to extend: adding a new case means adding a new case clause, not modifying existing conditionals.
One tradeoff is that patterns are coupled to the class's attribute names. Renaming an attribute breaks all patterns that use it. Using keyword patterns makes the coupling explicit and easier to update. Also, because patterns do not call the class constructor, they do not validate the instance's state; they only inspect existing attributes. This is usually desirable, but be aware that a malformed object might match a pattern unexpectedly.
When to Use Class Patterns vs. Alternatives
Class patterns are the right tool when you need to dispatch on both the type and the internal structure of an object. They shine in parsers, command handlers, and state machines. If you only need to check the type, a simple isinstance is lighter. If you need to transform data, dataclasses combined with pattern matching are a powerful combination.
For example, with a dataclass:
from dataclasses import dataclass @dataclass class Circle: radius: float @dataclass class Square: side: float def area(shape): match shape: case Circle(radius=r): return 3.14159 * r * r case Square(side=s): return s * s
Dataclasses provide the positional attributes automatically, making the pattern concise. This pattern is common in functional-style Python code.
Avoid class patterns when the object's attributes are dynamic or when you need to match based on computed properties. In those cases, a guard with a function call is more flexible. Also, if the class hierarchy is deep and you need to match many subclasses, consider using a registry or a dictionary of callables instead of a long match statement.