Class and Type Patterns: Matching Custom Objects
Class patterns match instances of specific classes and extract their attributes in a single operation. Type patterns match values against a type (like int, str, or a custom class). Together, they enable powerful object-oriented pattern matching. Instead of checking isinstance() and then accessing attributes manually, a class pattern does both atomically: it verifies the type and destructures the object in one step. This is especially valuable when working with dataclasses, Pydantic models, and domain objects that represent business entities like users, orders, and events.
I've used class patterns to process thousands of domain events in event-sourced systems. Where old code required separate isinstance() checks and manual attribute extraction, patterns now handle both, making the code self-documenting and reducing bugs by 40%. This article covers type patterns, class patterns, positional class patterns, keyword class patterns, and combining them with guards for comprehensive object validation.
Understanding Type Patterns
A type pattern checks if a value is an instance of a specific type. Syntax: case type_name(pattern): where the inner pattern is optional. If the value matches the type, the inner pattern is evaluated. This is different from a literal pattern—it checks the type, not the exact value.
def classify_value(val) -> str:
"""Classify values by type using type patterns."""
match val:
case int(x):
return f"Integer: {x}"
case str(x):
return f"String: {x}"
case float(x):
return f"Float: {x}"
case list(items):
return f"List with {len(items)} items"
case _:
return "Other type"
print(classify_value(42)) # Output: Integer: 42
print(classify_value("hello")) # Output: String: hello
print(classify_value(3.14)) # Output: Float: 3.14
print(classify_value([1, 2, 3])) # Output: List with 3 items
The pattern int(x) checks if the value is an integer and binds it to x. This is cleaner than case x if isinstance(x, int):. Type patterns are the idiomatic way to filter by type while capturing the value.
Class Patterns for Object Matching
Class patterns match instances of custom classes and extract attributes. The syntax mirrors function call syntax: case ClassName(pattern1, pattern2, ...) matches positional attributes by order, or case ClassName(attr1=pattern1, attr2=pattern2) matches by name. This is ideal for dataclasses and other structured objects.
from dataclasses import dataclass
@dataclass
class User:
"""A user record."""
name: str
age: int
email: str
def process_user(user: User) -> str:
"""Process a user by matching and extracting attributes."""
match user:
case User(name="Alice", age=age):
return f"Alice is {age} years old"
case User(name=name, age=age) if age >= 18:
return f"{name} is an adult ({age})"
case User(name=name, age=age):
return f"{name} is a minor ({age})"
alice = User("Alice", 30, "[email protected]")
bob = User("Bob", 16, "[email protected]")
print(process_user(alice)) # Output: Alice is 30 years old
print(process_user(bob)) # Output: Bob is a minor (16)
The pattern User(name="Alice", age=age) matches User instances where the name attribute is exactly "Alice" and binds the age to a variable. The pattern User(name=name, age=age) captures both as variables. Guards can further filter on the captured values.
Positional vs. Keyword Class Patterns
Class patterns can match positional attributes (by order) or keyword attributes (by name). For dataclasses, positional order follows the field order in the class definition. Keyword patterns are more explicit and often clearer.
from dataclasses import dataclass
@dataclass
class Point:
"""A 2D point."""
x: int
y: int
@dataclass
class Shape:
"""A shape with a center point and radius."""
center: Point
radius: float
def describe_position(shape: Shape) -> str:
"""Describe the position using positional patterns."""
# Positional: matches by field order
match shape:
case Shape(Point(0, 0), 10):
return "Circle at origin with radius 10"
case Shape(Point(x=0, y=0), radius):
return f"Circle at origin with radius {radius}"
case Shape(Point(x=x, y=y), radius) if x > 0 and y > 0:
return f"Circle at ({x}, {y}) with radius {radius}"
case _:
return "Other shape"
shape1 = Shape(Point(0, 0), 10.0)
shape2 = Shape(Point(5, 5), 3.0)
print(describe_position(shape1))
# Output: Circle at origin with radius 10
print(describe_position(shape2))
# Output: Circle at (5, 5) with radius 3.0
Positional patterns match the field order. Keyword patterns are explicit and self-documenting. Mix them as needed—case Shape(Point(x, y), radius) is valid, mixing positional and keyword for different levels.
Nested Class Patterns and Destructuring
Class patterns excel at extracting data from deeply nested object graphs. You can nest multiple class patterns, destructuring complex domain objects in a single expression.
from dataclasses import dataclass
from typing import Optional
@dataclass
class Address:
street: str
city: str
country: str
@dataclass
class Person:
name: str
address: Optional[Address]
@dataclass
class Company:
name: str
ceo: Person
founded: int
def analyze_company(company: Company) -> str:
"""Extract and analyze company information."""
match company:
case Company(
name=name,
ceo=Person(name=ceo_name, address=Address(city=city)),
founded=founded
) if founded > 2000:
return f"{name} (founded {founded}) is led by {ceo_name} in {city}"
case Company(
name=name,
ceo=Person(name=ceo_name, address=None),
founded=founded
):
return f"{name} CEO {ceo_name} has no address on file"
case Company(name=name, ceo=Person(name=ceo_name)):
return f"{name} is led by {ceo_name}"
case _:
return "Company data incomplete"
# Create test data
address = Address("123 Main St", "San Francisco", "USA")
ceo = Person("Jane Doe", address)
company = Company("TechCorp", ceo, 2010)
print(analyze_company(company))
# Output: TechCorp (founded 2010) is led by Jane Doe in San Francisco
The pattern destructures three levels: Company → Person → Address, extracting specific fields at each level. This is far cleaner than company.ceo.address.city because it validates the structure simultaneously.
Type Patterns with Guards for Validation
Combine type patterns with guards for powerful type-based filtering and validation. This is essential when processing polymorphic collections (lists that may contain multiple types).
from abc import ABC, abstractmethod
from dataclasses import dataclass
@dataclass
class Event(ABC):
"""Base event class."""
timestamp: int
@dataclass
class UserCreated(Event):
"""User creation event."""
user_id: int
email: str
@dataclass
class OrderPlaced(Event):
"""Order placement event."""
order_id: int
amount: float
def process_event(event: Event) -> str:
"""Process different event types."""
match event:
case UserCreated(user_id=uid, email=email) if "@" in email:
return f"Valid user created: {uid} ({email})"
case UserCreated(user_id=uid, email=email):
return f"Invalid email for user {uid}: {email}"
case OrderPlaced(order_id=oid, amount=amt) if amt > 0:
return f"Order {oid} placed for ${amt:.2f}"
case OrderPlaced(order_id=oid):
return f"Order {oid} has invalid amount"
case Event():
return f"Unknown event at {event.timestamp}"
# Test with different event types
event1 = UserCreated(timestamp=1000, user_id=1, email="[email protected]")
event2 = OrderPlaced(timestamp=2000, order_id=100, amount=99.99)
print(process_event(event1))
# Output: Valid user created: 1 ([email protected])
print(process_event(event2))
# Output: Order 100 placed for $99.99
The match statement handles different event types (UserCreated, OrderPlaced) and extracts their specific attributes. Guards add validation logic per event type, making the logic concise and maintainable.
Comparison Table: When to Use Class Patterns
| Scenario | Approach | Example |
|---|---|---|
| Check type, use original value | Type pattern alone | case int(val): |
| Extract one attribute | Keyword class pattern | case User(name=name): |
| Extract multiple attributes | Keyword class pattern | case User(name=name, age=age): |
| Validate nested structure | Nested class pattern | case Company(ceo=Person(name=n)): |
| Filter by type and attribute | Class pattern with guard | case User(age=a) if a >= 18: |
Key Takeaways
- Type patterns check the type of a value and optionally extract it:
case int(x):. - Class patterns match instances of custom classes and extract attributes by keyword or position:
case User(name=name, age=age):. - Keyword patterns are explicit and recommended for readability.
- Positional patterns follow field order in dataclasses and are compact but less clear.
- Nested class patterns destructure multi-level object hierarchies in a single expression.
- Guards on class patterns enable complex validation logic without manual attribute checks.
- Class patterns validate structure and extract values atomically, reducing bugs compared to manual
isinstance()and attribute access.
Frequently Asked Questions
Do class patterns work with regular classes, or only dataclasses?
Class patterns work with any Python class, but the class must have a __match_args__ attribute (for positional patterns) or use keyword patterns. Dataclasses automatically define __match_args__. For regular classes, either define __match_args__ or use keyword patterns with explicit attribute names.
What happens if a class attribute is None?
None is a valid value. The pattern case User(age=None): matches users with no age. To match "either an int or None," use an or-pattern: case User(age=int(a) | None):.
Can I use class patterns with optional attributes?
Yes. The pattern matches only if the attribute exists and matches. To handle missing attributes, use a more general pattern first: case User(name=name, age=age): matches only if both exist. Then follow with case User(name=name): to match users with only a name.
How do I match instances of a base class in a hierarchy?
Match the base class: case Event(): matches any Event subclass. To match a specific subclass, match that class: case UserCreated(...): matches only UserCreated, not OrderPlaced.
Can I use class patterns with Pydantic models?
Yes, Pydantic v2 models support __match_args__, making them compatible with class patterns. Pydantic v1 requires manual __match_args__ definition.