Skip to content

Python Dataclasses (@dataclass)

A dataclass is a class that mainly stores data. The @dataclass decorator (from the dataclasses module, Python 3.7+) writes the boilerplate for you: __init__, __repr__, __eq__, and more — generated from the field annotations.

quickstart.py
from dataclasses import dataclass
 
@dataclass
class Point:
    x: int
    y: int
 
p = Point(3, 4)
print(p)            # Point(x=3, y=4)   <- free __repr__
print(p.x, p.y)     # 3 4
print(p == Point(3, 4))   # True        <- free __eq__

Without dataclasses, that Point would need a hand-written __init__, __repr__, and __eq__. The decorator generates them from the annotated fields:

You write@dataclass generates
x: intA parameter in __init__ and an attribute.
(nothing)__repr__Point(x=3, y=4).
(nothing)__eq__ → field-by-field equality.
order=True<, <=, >, >= comparisons.
frozen=TrueImmutable, hashable instances.

Give fields defaults just like function parameters. Fields with defaults must come after those without.

defaults.py
from dataclasses import dataclass
 
@dataclass
class User:
    name: str
    active: bool = True
    role: str = "member"
 
print(User("Ada"))                  # User(name='Ada', active=True, role='member')
print(User("Bo", role="admin"))    # User(name='Bo', active=True, role='admin')

You cannot use a mutable default (like [] or {}) directly — it would be shared across all instances. Use field(default_factory=...).

default_factory.py
from dataclasses import dataclass, field
 
@dataclass
class Cart:
    items: list = field(default_factory=list)   # a fresh list per instance
    tags: dict = field(default_factory=dict)
 
a = Cart()
a.items.append("apple")
b = Cart()
print(a.items)   # ['apple']
print(b.items)   # []   <- not shared, thanks to default_factory

Writing items: list = [] raises a ValueError in dataclasses precisely to prevent the shared-mutable-default bug.

frozen=True makes instances read-only and hashable — usable as dict keys or set members.

frozen.py
from dataclasses import dataclass
 
@dataclass(frozen=True)
class Coord:
    lat: float
    lon: float
 
c = Coord(51.5, -0.1)
print(c)                 # Coord(lat=51.5, lon=-0.1)
locations = {c: "London"}   # works: frozen instances are hashable
# c.lat = 0.0            # would raise FrozenInstanceError

order=True adds comparison operators based on the fields (compared as a tuple, top to bottom).

order.py
from dataclasses import dataclass
 
@dataclass(order=True)
class Version:
    major: int
    minor: int
 
versions = [Version(2, 0), Version(1, 5), Version(1, 9)]
print(sorted(versions))
# [Version(major=1, minor=5), Version(major=1, minor=9), Version(major=2, minor=0)]

post_init — validation and derived fields

Section titled “post_init — validation and derived fields”

__post_init__ runs right after the generated __init__, perfect for validation or computing derived values.

post_init.py
from dataclasses import dataclass
 
@dataclass
class Rectangle:
    width: float
    height: float
    area: float = 0.0
 
    def __post_init__(self):
        if self.width < 0 or self.height < 0:
            raise ValueError("dimensions must be non-negative")
        self.area = self.width * self.height
 
r = Rectangle(3, 4)
print(r.area)    # 12.0
asdict.py
from dataclasses import dataclass, asdict, astuple
 
@dataclass
class Point:
    x: int
    y: int
 
p = Point(1, 2)
print(asdict(p))    # {'x': 1, 'y': 2}
print(astuple(p))   # (1, 2)
sketch Where a plain object spends its memory p5.js
Every ordinary instance carries a __dict__ so you can attach attributes at any time. That dictionary is most of the object: two integer attributes measured 48 bytes for the object plus 296 for its __dict__, 344 in total. Declaring __slots__ removes the dictionary and stores the attributes in fixed positions -- the same instance drops to 48 bytes, a saving of 86 percent. A dataclass is a normal class and gains nothing here unless you ask for slots explicitly.
NeedUse
A mutable record with methods@dataclass
An immutable, hashable record@dataclass(frozen=True)
A tiny immutable record, tuple-likenamedtuple
Arbitrary, schemaless dataa plain dict
  • Mutable defaults — never x: list = []; use field(default_factory=list).
  • Field order — non-default fields must precede default ones.
  • Equality is field-based — two instances with equal fields are == (usually what you want).
  • frozen=True blocks attribute assignment — set everything via __init__/__post_init__.
  • @dataclass auto-generates __init__, __repr__, and __eq__ from field annotations.
  • Use defaults like function parameters; use field(default_factory=...) for mutable defaults.
  • frozen=True makes immutable, hashable records; order=True adds comparisons.
  • __post_init__ validates or computes derived fields; asdict/astuple convert instances.
  • Reach for dataclasses whenever a class is mostly structured data.

pch.coffeeTagline

pch.coffeeCta

pch.feedbackHeading

pch.feedbackSubheading