name: dataclass-patterns description: Dataclass patterns including frozen dataclasses, slots, immutability, and value objects. Activated when designing data classes or value types.
Dataclass patterns
Purpose
Guide for designing dataclasses including frozen (immutable) dataclasses, slots optimization, validation, and value object patterns.
When to use
This skill activates when:
- Creating data classes
- Designing immutable value objects
- Optimizing memory usage with slots
- Implementing validation in dataclasses
- Using dataclass features like field factories
Core patterns
Frozen immutable dataclass
from dataclasses import dataclass
@dataclass(frozen=True, slots=True)
class Point:
"""Immutable point with memory-efficient slots."""
x: float
y: float
# Usage
p = Point(1.0, 2.0)
# p.x = 3.0 # Raises FrozenInstanceError
With validation
from dataclasses import dataclass
@dataclass(frozen=True, slots=True)
class PositivePoint:
"""Point with validation."""
x: float
y: float
def __post_init__(self) -> None:
if self.x < 0 or self.y < 0:
raise ValueError(f"Coordinates must be positive: ({self.x}, {self.y})")
With field defaults
from dataclasses import dataclass, field
@dataclass(frozen=True, slots=True)
class Config:
"""Configuration with defaults."""
name: str
enabled: bool = True
options: tuple[str, ...] = field(default_factory=tuple)
metadata: dict[str, str] = field(default_factory=dict)
Slots optimization
Why use slots
# Without slots: each instance has __dict__ for attributes
@dataclass
class RegularPoint:
x: float
y: float
# 120+ bytes per instance
# With slots: fixed attribute storage
@dataclass(slots=True)
class SlottedPoint:
x: float
y: float
# ~50 bytes per instance
Slots with inheritance
@dataclass(slots=True)
class Base:
x: int
@dataclass(slots=True)
class Derived(Base):
y: int
# Python 3.10+ handles slots inheritance correctly
Immutability patterns
Frozen with copy modification
from dataclasses import dataclass, replace
@dataclass(frozen=True, slots=True)
class User:
id: int
name: str
active: bool
# Modify by creating new instance
user = User(id=1, name="Alice", active=True)
updated = replace(user, active=False)
assert user.active is True
assert updated.active is False
Deeply immutable
from dataclasses import dataclass, field
@dataclass(frozen=True, slots=True)
class ImmutableConfig:
"""Deeply immutable config using tuples instead of lists."""
name: str
options: tuple[str, ...] = field(default_factory=tuple)
@classmethod
def from_list(cls, name: str, options: list[str]) -> 'ImmutableConfig':
"""Create from list, converting to tuple."""
return cls(name=name, options=tuple(options))
Field patterns
Field with factory
from dataclasses import dataclass, field
from datetime import datetime
@dataclass(frozen=True, slots=True)
class Event:
name: str
timestamp: datetime = field(default_factory=datetime.now)
tags: frozenset[str] = field(default_factory=frozenset)
Field with metadata
from dataclasses import dataclass, field
@dataclass(frozen=True, slots=True)
class FormField:
name: str
value: str = ""
required: bool = field(default=False, metadata={'form': 'checkbox'})
max_length: int = field(default=100, metadata={'form': 'hidden'})
Exclude from repr/compare
from dataclasses import dataclass, field
@dataclass(frozen=True, slots=True)
class CachedResult:
key: str
value: str
# Cache metadata not part of equality or repr
_cache_time: float = field(repr=False, compare=False, default=0.0)
Validation patterns
Post-init validation
from dataclasses import dataclass
@dataclass(frozen=True, slots=True)
class Range:
start: int
end: int
def __post_init__(self) -> None:
if self.start > self.end:
raise ValueError(f"start ({self.start}) must be <= end ({self.end})")
Factory method validation
from dataclasses import dataclass
@dataclass(frozen=True, slots=True)
class Email:
"""Validated email address."""
address: str
def __post_init__(self) -> None:
if '@' not in self.address:
raise ValueError(f"Invalid email: {self.address}")
@classmethod
def parse(cls, value: str) -> 'Email':
"""Parse and validate email string."""
return cls(address=value.strip().lower())
Comparison and ordering
Custom ordering
from dataclasses import dataclass
from functools import total_ordering
@total_ordering
@dataclass(frozen=True, slots=True, eq=True)
class Version:
major: int
minor: int
patch: int
def __lt__(self, other: 'Version') -> bool:
return (self.major, self.minor, self.patch) < (other.major, other.minor, other.patch)
Hash for use in sets/dicts
@dataclass(frozen=True, slots=True)
class HashableItem:
"""Frozen dataclass is automatically hashable."""
id: str
name: str
# Can be used in sets and as dict keys
items = {HashableItem("1", "a"), HashableItem("2", "b")}
lookup = {HashableItem("1", "a"): "value"}
Pattern: Value object
from dataclasses import dataclass
@dataclass(frozen=True, slots=True)
class Money:
"""Immutable value object representing money."""
amount: int # In cents to avoid float issues
currency: str
def __post_init__(self) -> None:
if self.amount < 0:
raise ValueError("Amount cannot be negative")
if len(self.currency) != 3:
raise ValueError("Currency must be 3-letter code")
def add(self, other: 'Money') -> 'Money':
if self.currency != other.currency:
raise ValueError("Cannot add different currencies")
return Money(self.amount + other.amount, self.currency)
def __str__(self) -> str:
return f"{self.amount / 100:.2f} {self.currency}"
Checklist
- Use
frozen=Truefor immutable data - Use
slots=Truefor memory efficiency - Validation in
__post_init__or factory methods - Use
tuple/frozensetfor immutable collections - Use
replace()for modifications - Document invariants
Additional resources: