Python Match Case Class Pattern
python match case class pattern: Learn how to use Python's match statement with class patterns to match on type and attributes, capture values, and write readable cond...
Python's match case statement, introduced in Python 3.10, provides structural pattern matching. The python match case class pattern approach lets you match an object against a class and extract its attributes in a single step. This is more expressive than a chain of isinstance checks and attribute accesses, especially when dealing with a known set of types.
Consider a typical situation where you receive an event object that can be one of several types. Without pattern matching, you write nested conditionals. With class patterns, the structure of the match directly reflects the types you expect.
How Class Patterns Work in match Statements
A class pattern looks like ClassName(attr1=pattern1, attr2=pattern2). When the match subject is an instance of ClassName, Python evaluates each attribute pattern against the corresponding attribute of the subject. If all subpatterns match, the case succeeds. If the subject is not an instance of the class, the case fails without raising an exception.
The class pattern does not call the class constructor. It only checks isinstance and then accesses attributes. This means the class must be accessible in the current scope, and the attribute names must exist on the instance. If an attribute is missing, the pattern fails rather than raising an AttributeError.
Here is a minimal example using a dataclass:
from dataclasses import dataclass @dataclass class Point: x: int y: int def describe(point): match point: case Point(x=0, y=0): return "origin" case Point(x=0, y=y): return f"on y-axis at {y}" case Point(x=x, y=0): return f"on x-axis at {x}" case Point(x=x, y=y): return f"point ({x}, {y})" case _: return "not a Point"
The case Point(x=0, y=0) matches only when the subject is a Point and both attributes equal zero. The case Point(x=0, y=y) captures the value of y into the variable y, which can then be used in the case body. This combines type checking and destructuring into one readable pattern.
Matching on Class Type and Attributes
Class patterns require the class to be a valid Python identifier. You can use any class, including built-in types, user-defined classes, or classes from third-party libraries. The pattern checks isinstance(subject, ClassName), so subclass instances also match. This is consistent with normal Python type checking.
You can mix literal patterns with capture patterns inside the parentheses. Literal patterns like 0 or "red" are compared with equality. Capture patterns bind the attribute value to a variable. The variable name must be a valid identifier and cannot be a dotted name.
For example:
class Circle: def __init__(self, radius): self.radius = radius class Square: def __init__(self, side): self.side = side def area(shape): match shape: case Circle(radius=r): return 3.14159 * r * r case Square(side=s): return s * s case _: raise ValueError(f"Unknown shape: {shape!r}")
This avoids explicit isinstance calls and manual attribute extraction. The pattern Circle(radius=r) binds r to shape.radius only if shape is a Circle instance. The code is shorter and the intent is clear.
Using Positional Patterns with Classes
Class patterns can also use positional arguments, which map to attributes in the order defined by the class. For dataclasses, the order is the field order. For regular classes, positional patterns rely on the presence of a __match_args__ attribute. If __match_args__ is not defined, positional patterns are not allowed and will raise a TypeError at runtime.
Dataclasses automatically define __match_args__ based on field order. For example:
@dataclass class Point: x: int y: int match point: case Point(0, 0): print("origin") case Point(0, y): print(f"y-axis at {y}")
Here Point(0, y) is equivalent to Point(x=0, y=y). Positional patterns are concise when the attribute order is obvious. For a custom class, you can define __match_args__ to control which attributes are matched positionally:
class Vector: __match_args__ = ("x", "y", "z") def __init__(self, x, y, z): self.x = x self.y = y self.z = z
Without __match_args__, using a positional class pattern raises an error. This is a common source of confusion when porting code from dataclasses to plain classes.
Capturing Values with As Patterns
Sometimes you need to match a class pattern and also bind the entire object to a variable. The as pattern does this: case Point(x=0, y=0) as origin:. This binds the whole subject to origin if the pattern succeeds. It is useful when you need both the extracted attributes and the original object.
match event: case MouseClick(x, y) as click: log(f"click at {click.x}, {click.y}") case KeyPress(key) as press: log(f"key {press.key} pressed")
The as pattern works with any pattern, not just class patterns. It is especially helpful when the class has additional methods or state that you need to call after matching.
Combining Class Patterns with Guards
A guard is an if condition attached to a case. It adds an extra check after the pattern matches. Guards are useful when the pattern alone cannot express the required condition, such as comparing two captured values.
match point: case Point(x, y) if x == y: print("on diagonal") case Point(x, y): print("not on diagonal")
The guard if x == y is evaluated only after the pattern matches. If the guard evaluates to false, the case is not selected and matching continues to the next case. Guards can access variables bound in the pattern.
Be careful with guards that have side effects or raise exceptions. The guard is evaluated during matching, so an exception in a guard will propagate out of the match statement. Keep guards simple and free of I/O.
Common Mistakes and Runtime Behavior
A frequent mistake is using a class pattern with a variable name that is not defined in scope. The class name in a pattern is looked up as a variable. If you intend to match any object and capture it, use a capture pattern (a bare name) instead of a class pattern.
Another mistake is assuming that a class pattern will call the class constructor. It does not. The pattern only checks isinstance and reads attributes. This is usually what you want, but it means the class must be importable and the attributes must be accessible.
When an attribute is missing, the pattern fails silently. For example, if a class has an optional attribute that is not always set, a class pattern referencing that attribute will not match. This is different from accessing the attribute directly, which would raise an AttributeError. Understanding this behavior helps you design patterns that are robust to partial data.
Pattern matching is evaluated from top to bottom. The first matching case is selected. This means you should order cases from most specific to least specific. A catch-all case with _ is common at the end.
Performance and Maintainability Considerations
Class patterns are implemented in the Python interpreter and are generally as fast as equivalent isinstance checks followed by attribute access. The overhead is minimal because the pattern matching machinery is optimized in CPython. There is no need to worry about performance for typical dispatch logic.
The real benefit is maintainability. A match statement groups all type-based branches in one place, making the logic easier to read and modify. Adding a new class requires adding a new case rather than editing a chain of if-elif statements. This is particularly valuable in libraries that process a family of related types.
One tradeoff is that pattern matching is a newer feature. If your codebase must support Python versions before 3.10, you cannot use it without a backport library or a different approach. For projects that already require Python 3.10 or later, class patterns are a clean, idiomatic choice.
Another consideration is that class patterns rely on attribute access, which can trigger __getattr__ if the class defines it. This could have side effects. In most cases, classes are simple data holders, but be aware of this when matching on objects with dynamic attribute behavior.
When you need to match on a class but also validate relationships between attributes, guards are the right tool. For example, matching a Rectangle and checking that width is greater than height requires a guard. This keeps the pattern itself simple and the condition explicit.
Class patterns work well with dataclasses, namedtuples, and other classes that define __match_args__. For classes without it, use keyword patterns. This flexibility makes python match case class pattern a versatile feature for writing expressive, type-aware code.