Site

Dataclasses — Python Dataclasses

Tutorial S10  •  Python / Learn

S10.0 What This Teaches

This tutorial covers Python's dataclasses module:

S10.1 Basic @dataclass

Decorating a class with @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
Without @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()

Use 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'])
Never use a mutable default directly (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
Fields are compared left to right - the first field is the most significant. Combine with 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
Use 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) called RGB with fields r, g, b (all int). Validate in __post_init__ that all values are in 0-255. Add a method blend(other: RGB) -> RGB that averages the two colors.
  • Create a @dataclass(order=True) called Student with gpa: float and name: str. Sort a list of students by GPA descending, then by name ascending.
  • Write a @dataclass with a list field using field(default_factory=list). Show that two instances do not share the same list (common bug without field()).

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

TermMeaning
@dataclassDecorator that generates __init__, __repr__, __eq__ from annotated fields
field()Configures per-field behavior: default_factory, repr, compare, init
default_factoryCallable invoked to produce a fresh default value per instance
frozen=TrueMakes instances immutable and hashable; raises FrozenInstanceError on mutation
__post_init__Called after __init__; use for validation and derived field computation
order=TrueGenerates comparison methods (__lt__, __le__, etc.) based on field order
slots=TrueAdds __slots__ to reduce memory and speed up attribute access (Python 3.10+)
value equalityTwo instances are equal when all compared fields have equal values