S10.0 What This Teaches
dataclasses module:@dataclass: auto-generating__init__,__repr__,__eq__field()for default values and factoriesfrozen=Truefor immutable dataclasses__post_init__for validation and derived fields- Ordering with
order=True slots=Truefor memory-efficient instances
S10.1 Basic @dataclass
@dataclass generates __init__,
__repr__, and __eq__ from the annotated fields:
# Dataclasses - auto-generated boilerplate from field annotations.
from dataclasses import dataclass
@dataclass
class Person:
name: str
age: int
alice = Person("Alice", 30)
bob = Person("Bob", 25)
print(alice) # Person(name='Alice', age=30)
print(alice == Person("Alice", 30)) # True - value equality
print(alice == bob) # False
@dataclass, you would write __init__,
__repr__, and __eq__ by hand. The decorator generates
all three from the type-annotated class body.
S10.2 Default Values and field()
field() when you need a factory for mutable defaults, or to
control which fields appear in __repr__ and __init__:
from dataclasses import dataclass, field
@dataclass
class Config:
host: str = "localhost"
port: int = 8080
tags: list = field(default_factory=list) # new list per instance
_secret: str = field(default="", repr=False, compare=False)
c1 = Config()
c2 = Config()
c1.tags.append("web")
print(c1.tags) # ['web']
print(c2.tags) # [] - independent list, not shared
print(c1) # Config(host='localhost', port=8080, tags=['web'])
tags: list = []) - that
shares one list across all instances. Always use field(default_factory=list).
S10.3 Frozen Dataclasses
frozen=True makes the dataclass immutable and hashable, so instances
can be used as dictionary keys or in sets:
from dataclasses import dataclass
@dataclass(frozen=True)
class Point:
x: float
y: float
p = Point(3.0, 4.0)
# p.x = 9.0 # FrozenInstanceError - cannot assign
# Hashable - usable in sets and as dict keys
points = {Point(0, 0), Point(1, 1), Point(0, 0)}
print(points) # {Point(x=0, y=0), Point(x=1, y=1)}
lookup = {Point(1, 0): "east", Point(0, 1): "north"}
print(lookup[Point(1, 0)]) # east
S10.4 __post_init__ for Validation
__post_init__ runs after the generated __init__.
Use it for validation and for computing derived fields:
from dataclasses import dataclass, field
import math
@dataclass
class Circle:
radius: float
def __post_init__(self):
if self.radius <= 0:
raise ValueError(f"radius must be positive, got {self.radius}")
@property
def area(self) -> float:
return math.pi * self.radius ** 2
c = Circle(5.0)
print(f"area = {c.area:.2f}") # area = 78.54
try:
Circle(-1)
except ValueError as e:
print(e) # radius must be positive, got -1
S10.5 Ordering with order=True
order=True generates __lt__, __le__,
__gt__, and __ge__ based on the field order:
from dataclasses import dataclass
@dataclass(order=True)
class Version:
major: int
minor: int
patch: int
versions = [Version(1, 10, 0), Version(2, 0, 0), Version(1, 9, 5)]
print(sorted(versions))
# [Version(major=1, minor=9, patch=5),
# Version(major=1, minor=10, patch=0),
# Version(major=2, minor=0, patch=0)]
print(Version(1, 9, 5) < Version(1, 10, 0)) # True
frozen=True to get an immutable, comparable, hashable type.
S10.6 slots=True for Efficiency
slots=True (Python 3.10+) adds __slots__ to the class,
reducing memory per instance and speeding up attribute access:
from dataclasses import dataclass
import sys
@dataclass
class Normal:
x: float
y: float
@dataclass(slots=True)
class Slotted:
x: float
y: float
n = Normal(1.0, 2.0)
s = Slotted(1.0, 2.0)
print(sys.getsizeof(n.__dict__)) # ~232 bytes (dict overhead)
# Slotted has no __dict__ - attributes stored directly in slots
print(n == s) # True - same values
slots=True when creating many instances (data pipelines, game
entities) where memory and access speed matter.
S10.7 Example - All Together
# Dataclasses - immutable order model with validation and ordering.
from dataclasses import dataclass, field
from datetime import date
@dataclass(frozen=True, order=True)
class Product:
sku: str
name: str
price: float
def __post_init__(self):
if self.price < 0:
raise ValueError(f"price cannot be negative: {self.price}")
@dataclass
class Order:
customer: str
lines: list = field(default_factory=list)
date: date = field(default_factory=date.today)
def add(self, product: Product, qty: int):
self.lines.append((product, qty))
@property
def total(self) -> float:
return sum(p.price * q for p, q in self.lines)
widget = Product("W-01", "Widget", 9.99)
gadget = Product("G-01", "Gadget", 49.95)
order = Order("Alice")
order.add(widget, 3)
order.add(gadget, 1)
for product, qty in order.lines:
print(f" {product.name} x{qty}: ${product.price * qty:.2f}")
print(f"Total: ${order.total:.2f}")
S10.8 Exercise
Exercise
- Define a
@dataclass(frozen=True)calledRGBwith fieldsr,g,b(allint). Validate in__post_init__that all values are in 0-255. Add a methodblend(other: RGB) -> RGBthat averages the two colors. - Create a
@dataclass(order=True)calledStudentwithgpa: floatandname: str. Sort a list of students by GPA descending, then by name ascending. - Write a
@dataclasswith alistfield usingfield(default_factory=list). Show that two instances do not share the same list (common bug withoutfield()).
S10.9 Common Mistakes
Mutable default without field(default_factory=...)
@dataclass
class Bad:
items: list = [] # TypeError! Dataclass will refuse this at class definition time.
@dataclass
class Good:
items: list = field(default_factory=list) # correct - new list per instance
Expecting frozen to deep-freeze mutable contents
@dataclass(frozen=True)
class Bag:
contents: list
b = Bag([1, 2, 3])
b.contents.append(4) # works! frozen only prevents reassigning b.contents
print(b.contents) # [1, 2, 3, 4]
# Use a tuple instead of list for truly immutable contents
Using order=True without frozen=True on a hashed class
@dataclass(order=True)
class Point:
x: float
y: float
# Not hashable by default - can't use in sets or as dict key
# s = {Point(0, 0)} # TypeError: unhashable type: 'Point'
# Add frozen=True to get both ordering and hashing
S10.10 Key Terms
| Term | Meaning |
|---|---|
| @dataclass | Decorator that generates __init__, __repr__, __eq__ from annotated fields |
| field() | Configures per-field behavior: default_factory, repr, compare, init |
| default_factory | Callable invoked to produce a fresh default value per instance |
| frozen=True | Makes instances immutable and hashable; raises FrozenInstanceError on mutation |
| __post_init__ | Called after __init__; use for validation and derived field computation |
| order=True | Generates comparison methods (__lt__, __le__, etc.) based on field order |
| slots=True | Adds __slots__ to reduce memory and speed up attribute access (Python 3.10+) |
| value equality | Two instances are equal when all compared fields have equal values |