Skip to content

Python Dataclasses (@dataclass)

A dataclass is a class that mainly stores data. The @dataclass@dataclass decorator (from the dataclassesdataclasses module, Python 3.7+) writes the boilerplate for you: __init____init__, __repr____repr__, __eq____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__
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__

What it generates for you

Without dataclasses, that PointPoint would need a hand-written __init____init__, __repr____repr__, and __eq____eq__. The decorator generates them from the annotated fields:

You write@dataclass@dataclass generates
x: intx: intA parameter in __init____init__ and an attribute.
(nothing)__repr____repr__Point(x=3, y=4)Point(x=3, y=4).
(nothing)__eq____eq__ → field-by-field equality.
order=Trueorder=True<<, <=<=, >>, >=>= comparisons.
frozen=Truefrozen=TrueImmutable, hashable instances.

Default values

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')
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')

Mutable defaults — use default_factory

You cannot use a mutable default (like [][] or {}{}) directly — it would be shared across all instances. Use field(default_factory=...)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
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 = []items: list = [] raises a ValueErrorValueError in dataclasses precisely to prevent the shared-mutable-default bug.

Frozen (immutable) dataclasses

frozen=Truefrozen=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
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

Ordered dataclasses

order=Trueorder=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)]
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

__post_init____post_init__ runs right after the generated __init____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
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

Converting to dict or tuple

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)
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)

When to use what

NeedUse
A mutable record with methods@dataclass@dataclass
An immutable, hashable record@dataclass(frozen=True)@dataclass(frozen=True)
A tiny immutable record, tuple-likenamedtuplenamedtuple
Arbitrary, schemaless dataa plain dictdict

Common pitfalls

  • Mutable defaults — never x: list = []x: list = []; use field(default_factory=list)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=Truefrozen=True blocks attribute assignment — set everything via __init____init__/__post_init____post_init__.

Practice Exercises

Exercise 1 – Define a dataclass

Exercise 2 – Safe mutable default

Exercise 3 – Convert to a dict

Summary

  • @dataclass@dataclass auto-generates __init____init__, __repr____repr__, and __eq____eq__ from field annotations.
  • Use defaults like function parameters; use field(default_factory=...)field(default_factory=...) for mutable defaults.
  • frozen=Truefrozen=True makes immutable, hashable records; order=Trueorder=True adds comparisons.
  • __post_init____post_init__ validates or computes derived fields; asdictasdict/astupleastuple convert instances.
  • Reach for dataclasses whenever a class is mostly structured data.

If this helped you, consider buying me a coffee ☕

Buy me a coffee

Was this page helpful?

Let us know how we did