Dataclasses
1 · The lesson
readA class that exists mainly to hold data (a point, an order, a config) needs __init__, __repr__, and __eq__, written by hand every time. That's about ten lines of repeated code for what could be three. @dataclass generates all of it from the annotated fields, so you only write the behaviour that is specific to your class.
This lesson covers the decorator, the common mistake with field defaults, frozen and ordered dataclasses, __post_init__ validation, and when a dataclass is the wrong tool.
1. The Boilerplate Problem
Writing a plain class to hold three fields is mostly typing:
class Point: def __init__(self, x, y, z=0): self.x = x self.y = y self.z = z def __repr__(self): return f"Point(x={self.x}, y={self.y}, z={self.z})" def __eq__(self, other): if not isinstance(other, Point): return NotImplemented return (self.x, self.y, self.z) == (other.x, other.y, other.z)
That is twelve lines, and none of them is real behaviour. The dataclass version:
from dataclasses import dataclass @dataclass class Point: x: float y: float z: float = 0 p = Point(1, 2) print(p) # Point(x=1, y=2, z=0) print(p == Point(1, 2)) # True
@dataclass reads the annotated class-body attributes and generates __init__, __repr__, and __eq__. The type annotations are required. They are how Python knows which attributes are fields.
2. Field Defaults and default_factory
Simple defaults sit next to the annotation:
@dataclass class Config: host: str = "localhost" port: int = 5432 ssl: bool = False
setup added so this can run · re-runs 1 earlier block on this page first
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) dataclass = _AutoMock('dataclass') # Earlier blocks on this page that this one builds on, re-run quietly first. try: from dataclasses import dataclass except Exception: pass _pm_earlier = [ 'from dataclasses import dataclass\n\n@dataclass\nclass Point:\n x: float\n y: float\n z: float = 0\n\np = Point(1, 2)\nprint(p) # Point(x=1, y=2, z=0)\nprint(p == Point(1, 2)) # True', ] def _pm_run_earlier(_srcs, _g): import sys as _s, io as _io, warnings as _w for _src in _srcs: _o, _e = _s.stdout, _s.stderr _s.stdout = _s.stderr = _io.StringIO() try: with _w.catch_warnings(): _w.simplefilter('ignore') exec(compile(_src, '<earlier block>', 'exec'), _g) except (Exception, SystemExit): pass finally: _s.stdout, _s.stderr = _o, _e if 'matplotlib.pyplot' in _s.modules: _s.modules['matplotlib.pyplot'].close('all') # their plots belong to their own blocks _pm_run_earlier(_pm_earlier, globals()) del _pm_earlier, _pm_run_earlier
Mutable defaults are not allowed. If you try the obvious thing, Python raises an error:
@dataclass class Cart: items: list = [] # ValueError: mutable default <class 'list'> for field items # is not allowed: use default_factory
setup added so this can run · re-runs 1 earlier block on this page first
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) dataclass = _AutoMock('dataclass') # Earlier blocks on this page that this one builds on, re-run quietly first. try: from dataclasses import dataclass except Exception: pass _pm_earlier = [ 'from dataclasses import dataclass\n\n@dataclass\nclass Point:\n x: float\n y: float\n z: float = 0\n\np = Point(1, 2)\nprint(p) # Point(x=1, y=2, z=0)\nprint(p == Point(1, 2)) # True', ] def _pm_run_earlier(_srcs, _g): import sys as _s, io as _io, warnings as _w for _src in _srcs: _o, _e = _s.stdout, _s.stderr _s.stdout = _s.stderr = _io.StringIO() try: with _w.catch_warnings(): _w.simplefilter('ignore') exec(compile(_src, '<earlier block>', 'exec'), _g) except (Exception, SystemExit): pass finally: _s.stdout, _s.stderr = _o, _e if 'matplotlib.pyplot' in _s.modules: _s.modules['matplotlib.pyplot'].close('all') # their plots belong to their own blocks _pm_run_earlier(_pm_earlier, globals()) del _pm_earlier, _pm_run_earlier
This is the same problem as the mutable default argument from functions: one list would be shared by every Cart() instance. The fix is field(default_factory=...):
from dataclasses import dataclass, field @dataclass class Cart: items: list = field(default_factory=list) tags: set = field(default_factory=set) metadata: dict = field(default_factory=dict) a, b = Cart(), Cart() a.items.append("apple") print(a.items, b.items) # ['apple'] [] (each Cart has its own list)
default_factory takes a function with no arguments, and calls it once for each new instance. Pass list, dict or set directly, or lambda: ... for custom values.
3. frozen=True: Immutable Dataclasses
With frozen=True, you can't assign to an attribute after the object is created:
@dataclass(frozen=True) class Money: amount: int currency: str m = Money(100, "USD") print(m) # Money(amount=100, currency='USD') # m.amount = 200 # FrozenInstanceError: cannot assign to field 'amount'
setup added so this can run · re-runs 2 earlier blocks on this page first
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) def dataclass(*_a, **_kw): print('-> dataclass() called') return _AutoMock('dataclass()') # Earlier blocks on this page that this one builds on, re-run quietly first. try: from dataclasses import dataclass from dataclasses import dataclass, field except Exception: pass _pm_earlier = [ 'from dataclasses import dataclass\n\n@dataclass\nclass Point:\n x: float\n y: float\n z: float = 0\n\np = Point(1, 2)\nprint(p) # Point(x=1, y=2, z=0)\nprint(p == Point(1, 2)) # True', 'from dataclasses import dataclass, field\n\n@dataclass\nclass Cart:\n items: list = field(default_factory=list)\n tags: set = field(default_factory=set)\n metadata: dict = field(default_factory=dict)\n\na, b = Cart(), Cart()\na.items.append("apple")\nprint(a.items, b.items) # [\'apple\'] [] (each Cart has its own list)', ] def _pm_run_earlier(_srcs, _g): import sys as _s, io as _io, warnings as _w for _src in _srcs: _o, _e = _s.stdout, _s.stderr _s.stdout = _s.stderr = _io.StringIO() try: with _w.catch_warnings(): _w.simplefilter('ignore') exec(compile(_src, '<earlier block>', 'exec'), _g) except (Exception, SystemExit): pass finally: _s.stdout, _s.stderr = _o, _e if 'matplotlib.pyplot' in _s.modules: _s.modules['matplotlib.pyplot'].close('all') # their plots belong to their own blocks _pm_run_earlier(_pm_earlier, globals()) del _pm_earlier, _pm_run_earlier
This gives you two benefits:
- Hashable by default: you can put frozen dataclasses into a
setor use them asdictkeys. Regular@dataclassinstances are unhashable because they override__eq__. - Safe to share: if you pass a frozen instance through ten layers of code, you know that no code changed it.
prices = {Money(100, "USD"): "premium", Money(50, "USD"): "basic"}
print(prices[Money(100, "USD")]) # premiumsetup added so this can run · re-runs 3 earlier blocks on this page first
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) def Money(*_a, **_kw): print('-> Money() called') return _AutoMock('Money()') def dataclass(*_a, **_kw): print('-> dataclass() called') return _AutoMock('dataclass()') # Earlier blocks on this page that this one builds on, re-run quietly first. try: from dataclasses import dataclass from dataclasses import dataclass, field except Exception: pass _pm_earlier = [ 'from dataclasses import dataclass\n\n@dataclass\nclass Point:\n x: float\n y: float\n z: float = 0\n\np = Point(1, 2)\nprint(p) # Point(x=1, y=2, z=0)\nprint(p == Point(1, 2)) # True', 'from dataclasses import dataclass, field\n\n@dataclass\nclass Cart:\n items: list = field(default_factory=list)\n tags: set = field(default_factory=set)\n metadata: dict = field(default_factory=dict)\n\na, b = Cart(), Cart()\na.items.append("apple")\nprint(a.items, b.items) # [\'apple\'] [] (each Cart has its own list)', '@dataclass(frozen=True)\nclass Money:\n amount: int\n currency: str\n\nm = Money(100, "USD")\nprint(m) # Money(amount=100, currency=\'USD\')\n# m.amount = 200 # FrozenInstanceError: cannot assign to field \'amount\'', ] def _pm_run_earlier(_srcs, _g): import sys as _s, io as _io, warnings as _w for _src in _srcs: _o, _e = _s.stdout, _s.stderr _s.stdout = _s.stderr = _io.StringIO() try: with _w.catch_warnings(): _w.simplefilter('ignore') exec(compile(_src, '<earlier block>', 'exec'), _g) except (Exception, SystemExit): pass finally: _s.stdout, _s.stderr = _o, _e if 'matplotlib.pyplot' in _s.modules: _s.modules['matplotlib.pyplot'].close('all') # their plots belong to their own blocks _pm_run_earlier(_pm_earlier, globals()) del _pm_earlier, _pm_run_earlier
Use frozen dataclasses for value objects: objects that are defined only by their data (money, dates, points, coordinates).
Note: frozen=True is shallow. You can't rebind the dataclass's own attributes, but mutable objects inside it can still change:
@dataclass(frozen=True) class Bag: items: list = field(default_factory=list) b = Bag() b.items.append("apple") # works, because the list itself is not frozen print(b.items) # ['apple']
setup added so this can run · re-runs 2 earlier blocks on this page first
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) def dataclass(*_a, **_kw): print('-> dataclass() called') return _AutoMock('dataclass()') def field(*_a, **_kw): print('-> field() called') return _AutoMock('field()') # Earlier blocks on this page that this one builds on, re-run quietly first. try: from dataclasses import dataclass from dataclasses import dataclass, field except Exception: pass _pm_earlier = [ 'from dataclasses import dataclass\n\n@dataclass\nclass Point:\n x: float\n y: float\n z: float = 0\n\np = Point(1, 2)\nprint(p) # Point(x=1, y=2, z=0)\nprint(p == Point(1, 2)) # True', 'from dataclasses import dataclass, field\n\n@dataclass\nclass Cart:\n items: list = field(default_factory=list)\n tags: set = field(default_factory=set)\n metadata: dict = field(default_factory=dict)\n\na, b = Cart(), Cart()\na.items.append("apple")\nprint(a.items, b.items) # [\'apple\'] [] (each Cart has its own list)', ] def _pm_run_earlier(_srcs, _g): import sys as _s, io as _io, warnings as _w for _src in _srcs: _o, _e = _s.stdout, _s.stderr _s.stdout = _s.stderr = _io.StringIO() try: with _w.catch_warnings(): _w.simplefilter('ignore') exec(compile(_src, '<earlier block>', 'exec'), _g) except (Exception, SystemExit): pass finally: _s.stdout, _s.stderr = _o, _e if 'matplotlib.pyplot' in _s.modules: _s.modules['matplotlib.pyplot'].close('all') # their plots belong to their own blocks _pm_run_earlier(_pm_earlier, globals()) del _pm_earlier, _pm_run_earlier
If you need deep immutability, use tuples or frozen dataclasses all the way down.
4. order=True: Auto-Generated Ordering
order=True generates __lt__, __le__, __gt__ and __ge__. They compare the fields in the order you declared them, the same way tuples are compared:
@dataclass(order=True) class Version: major: int minor: int patch: int versions = [Version(1, 2, 0), Version(0, 9, 5), Version(1, 1, 9)] print(sorted(versions)) # [Version(major=0, minor=9, patch=5), Version(major=1, minor=1, patch=9), Version(major=1, minor=2, patch=0)]
setup added so this can run · re-runs 2 earlier blocks on this page first
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) def dataclass(*_a, **_kw): print('-> dataclass() called') return _AutoMock('dataclass()') # Earlier blocks on this page that this one builds on, re-run quietly first. try: from dataclasses import dataclass from dataclasses import dataclass, field except Exception: pass _pm_earlier = [ 'from dataclasses import dataclass\n\n@dataclass\nclass Point:\n x: float\n y: float\n z: float = 0\n\np = Point(1, 2)\nprint(p) # Point(x=1, y=2, z=0)\nprint(p == Point(1, 2)) # True', 'from dataclasses import dataclass, field\n\n@dataclass\nclass Cart:\n items: list = field(default_factory=list)\n tags: set = field(default_factory=set)\n metadata: dict = field(default_factory=dict)\n\na, b = Cart(), Cart()\na.items.append("apple")\nprint(a.items, b.items) # [\'apple\'] [] (each Cart has its own list)', ] def _pm_run_earlier(_srcs, _g): import sys as _s, io as _io, warnings as _w for _src in _srcs: _o, _e = _s.stdout, _s.stderr _s.stdout = _s.stderr = _io.StringIO() try: with _w.catch_warnings(): _w.simplefilter('ignore') exec(compile(_src, '<earlier block>', 'exec'), _g) except (Exception, SystemExit): pass finally: _s.stdout, _s.stderr = _o, _e if 'matplotlib.pyplot' in _s.modules: _s.modules['matplotlib.pyplot'].close('all') # their plots belong to their own blocks _pm_run_earlier(_pm_earlier, globals()) del _pm_earlier, _pm_run_earlier
Sometimes you want to sort by one field and ignore the others. Exclude a field from comparison with field(compare=False):
@dataclass(order=True) class Task: priority: int description: str = field(compare=False) # description is ignored when sorting tasks = [Task(3, "review PR"), Task(1, "fix bug"), Task(2, "write docs")] print(sorted(tasks)) # sorted by priority only
setup added so this can run · re-runs 2 earlier blocks on this page first
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) def dataclass(*_a, **_kw): print('-> dataclass() called') return _AutoMock('dataclass()') def field(*_a, **_kw): print('-> field() called') return _AutoMock('field()') # Earlier blocks on this page that this one builds on, re-run quietly first. try: from dataclasses import dataclass from dataclasses import dataclass, field except Exception: pass _pm_earlier = [ 'from dataclasses import dataclass\n\n@dataclass\nclass Point:\n x: float\n y: float\n z: float = 0\n\np = Point(1, 2)\nprint(p) # Point(x=1, y=2, z=0)\nprint(p == Point(1, 2)) # True', 'from dataclasses import dataclass, field\n\n@dataclass\nclass Cart:\n items: list = field(default_factory=list)\n tags: set = field(default_factory=set)\n metadata: dict = field(default_factory=dict)\n\na, b = Cart(), Cart()\na.items.append("apple")\nprint(a.items, b.items) # [\'apple\'] [] (each Cart has its own list)', ] def _pm_run_earlier(_srcs, _g): import sys as _s, io as _io, warnings as _w for _src in _srcs: _o, _e = _s.stdout, _s.stderr _s.stdout = _s.stderr = _io.StringIO() try: with _w.catch_warnings(): _w.simplefilter('ignore') exec(compile(_src, '<earlier block>', 'exec'), _g) except (Exception, SystemExit): pass finally: _s.stdout, _s.stderr = _o, _e if 'matplotlib.pyplot' in _s.modules: _s.modules['matplotlib.pyplot'].close('all') # their plots belong to their own blocks _pm_run_earlier(_pm_earlier, globals()) del _pm_earlier, _pm_run_earlier
5. __post_init__: Validation and Derived Fields
Sometimes you need code to run after the generated __init__ finishes: to validate input, compute derived fields, or normalise values. Define __post_init__:
@dataclass class Rectangle: width: float height: float area: float = field(init=False) # not a parameter of __init__ def __post_init__(self): if self.width <= 0 or self.height <= 0: raise ValueError("dimensions must be positive") self.area = self.width * self.height r = Rectangle(3, 4) print(r.area) # 12 # Rectangle(-1, 4) # ValueError
setup added so this can run · re-runs 2 earlier blocks on this page first
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) dataclass = _AutoMock('dataclass') def field(*_a, **_kw): print('-> field() called') return _AutoMock('field()') # Earlier blocks on this page that this one builds on, re-run quietly first. try: from dataclasses import dataclass from dataclasses import dataclass, field except Exception: pass _pm_earlier = [ 'from dataclasses import dataclass\n\n@dataclass\nclass Point:\n x: float\n y: float\n z: float = 0\n\np = Point(1, 2)\nprint(p) # Point(x=1, y=2, z=0)\nprint(p == Point(1, 2)) # True', 'from dataclasses import dataclass, field\n\n@dataclass\nclass Cart:\n items: list = field(default_factory=list)\n tags: set = field(default_factory=set)\n metadata: dict = field(default_factory=dict)\n\na, b = Cart(), Cart()\na.items.append("apple")\nprint(a.items, b.items) # [\'apple\'] [] (each Cart has its own list)', ] def _pm_run_earlier(_srcs, _g): import sys as _s, io as _io, warnings as _w for _src in _srcs: _o, _e = _s.stdout, _s.stderr _s.stdout = _s.stderr = _io.StringIO() try: with _w.catch_warnings(): _w.simplefilter('ignore') exec(compile(_src, '<earlier block>', 'exec'), _g) except (Exception, SystemExit): pass finally: _s.stdout, _s.stderr = _o, _e if 'matplotlib.pyplot' in _s.modules: _s.modules['matplotlib.pyplot'].close('all') # their plots belong to their own blocks _pm_run_earlier(_pm_earlier, globals()) del _pm_earlier, _pm_run_earlier
init=False keeps area out of the generated __init__ parameters. You don't pass it, because __post_init__ computes it.
6. Fine-Grained field() Control
field() accepts several options for individual attributes:
| Argument | Effect |
|---|---|
default=... | Simple default value |
default_factory=... | Zero-arg callable run per instance (for mutables) |
init=False | Don't include in __init__ parameters |
repr=False | Don't include in __repr__ output |
compare=False | Don't include in __eq__ or ordering |
hash=False | Don't include in __hash__ |
import time from dataclasses import dataclass, field @dataclass class User: username: str password_hash: str = field(repr=False) # not shown when you print a User created_at: float = field(default_factory=time.time, compare=False) cache: dict = field(default_factory=dict, repr=False, compare=False, init=False) u = User("surya", "$argon2id$...") print(u) # User(username='surya', created_at=<the current time>)
repr=False on secrets is a small security improvement. The value won't appear in logs when someone prints the object.
7. slots=True: Less Memory, Fixed Attributes (3.10+)
By default, Python classes store attributes in a per-instance __dict__, which is flexible but uses memory. slots=True replaces it with a fixed __slots__ declaration:
@dataclass(slots=True) class Point: x: float y: float p = Point(1, 2) # p.z = 3 # AttributeError: 'Point' object has no attribute 'z'
setup added so this can run · re-runs 3 earlier blocks on this page first
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) def dataclass(*_a, **_kw): print('-> dataclass() called') return _AutoMock('dataclass()') # Earlier blocks on this page that this one builds on, re-run quietly first. try: from dataclasses import dataclass from dataclasses import dataclass, field import time except Exception: pass _pm_earlier = [ 'from dataclasses import dataclass\n\n@dataclass\nclass Point:\n x: float\n y: float\n z: float = 0\n\np = Point(1, 2)\nprint(p) # Point(x=1, y=2, z=0)\nprint(p == Point(1, 2)) # True', 'from dataclasses import dataclass, field\n\n@dataclass\nclass Cart:\n items: list = field(default_factory=list)\n tags: set = field(default_factory=set)\n metadata: dict = field(default_factory=dict)\n\na, b = Cart(), Cart()\na.items.append("apple")\nprint(a.items, b.items) # [\'apple\'] [] (each Cart has its own list)', 'import time\nfrom dataclasses import dataclass, field\n\n@dataclass\nclass User:\n username: str\n password_hash: str = field(repr=False) # not shown when you print a User\n created_at: float = field(default_factory=time.time, compare=False)\n cache: dict = field(default_factory=dict, repr=False, compare=False, init=False)\n\nu = User("surya", "$argon2id$...")\nprint(u) # User(username=\'surya\', created_at=<the current time>)', ] def _pm_run_earlier(_srcs, _g): import sys as _s, io as _io, warnings as _w for _src in _srcs: _o, _e = _s.stdout, _s.stderr _s.stdout = _s.stderr = _io.StringIO() try: with _w.catch_warnings(): _w.simplefilter('ignore') exec(compile(_src, '<earlier block>', 'exec'), _g) except (Exception, SystemExit): pass finally: _s.stdout, _s.stderr = _o, _e if 'matplotlib.pyplot' in _s.modules: _s.modules['matplotlib.pyplot'].close('all') # their plots belong to their own blocks _pm_run_earlier(_pm_earlier, globals()) del _pm_earlier, _pm_run_earlier
Two effects:
- Lower memory: this matters when you create millions of instances.
- No new attributes: a typo in an attribute name raises an error when you assign it, not later.
The cost: such classes are harder to subclass, and some pickling and mixin patterns don't work with them. Use slots=True on data classes that you create in very large numbers, and skip it elsewhere.
8. asdict and astuple: Conversion Helpers
from dataclasses import asdict, astuple @dataclass class Point: x: int y: int p = Point(3, 4) print(asdict(p)) # {'x': 3, 'y': 4} print(astuple(p)) # (3, 4)
setup added so this can run · re-runs 3 earlier blocks on this page first
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) dataclass = _AutoMock('dataclass') # Earlier blocks on this page that this one builds on, re-run quietly first. try: from dataclasses import dataclass from dataclasses import dataclass, field import time except Exception: pass _pm_earlier = [ 'from dataclasses import dataclass\n\n@dataclass\nclass Point:\n x: float\n y: float\n z: float = 0\n\np = Point(1, 2)\nprint(p) # Point(x=1, y=2, z=0)\nprint(p == Point(1, 2)) # True', 'from dataclasses import dataclass, field\n\n@dataclass\nclass Cart:\n items: list = field(default_factory=list)\n tags: set = field(default_factory=set)\n metadata: dict = field(default_factory=dict)\n\na, b = Cart(), Cart()\na.items.append("apple")\nprint(a.items, b.items) # [\'apple\'] [] (each Cart has its own list)', 'import time\nfrom dataclasses import dataclass, field\n\n@dataclass\nclass User:\n username: str\n password_hash: str = field(repr=False) # not shown when you print a User\n created_at: float = field(default_factory=time.time, compare=False)\n cache: dict = field(default_factory=dict, repr=False, compare=False, init=False)\n\nu = User("surya", "$argon2id$...")\nprint(u) # User(username=\'surya\', created_at=<the current time>)', ] def _pm_run_earlier(_srcs, _g): import sys as _s, io as _io, warnings as _w for _src in _srcs: _o, _e = _s.stdout, _s.stderr _s.stdout = _s.stderr = _io.StringIO() try: with _w.catch_warnings(): _w.simplefilter('ignore') exec(compile(_src, '<earlier block>', 'exec'), _g) except (Exception, SystemExit): pass finally: _s.stdout, _s.stderr = _o, _e if 'matplotlib.pyplot' in _s.modules: _s.modules['matplotlib.pyplot'].close('all') # their plots belong to their own blocks _pm_run_earlier(_pm_earlier, globals()) del _pm_earlier, _pm_run_earlier
Both are recursive: a dataclass that contains other dataclasses becomes a nested dict or tuple. asdict is one of the cleanest ways to pass a dataclass to json.dumps:
import json print(json.dumps(asdict(p))) # {"x": 3, "y": 4}
setup added so this can run · re-runs 5 earlier blocks on this page first
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) def asdict(*_a, **_kw): print('-> asdict() called') return _AutoMock('asdict()') p = _AutoMock('p') def dataclass(*_a, **_kw): print('-> dataclass() called') return _AutoMock('dataclass()') # Earlier blocks on this page that this one builds on, re-run quietly first. try: from dataclasses import dataclass from dataclasses import dataclass, field import time from dataclasses import asdict, astuple except Exception: pass _pm_earlier = [ 'from dataclasses import dataclass\n\n@dataclass\nclass Point:\n x: float\n y: float\n z: float = 0\n\np = Point(1, 2)\nprint(p) # Point(x=1, y=2, z=0)\nprint(p == Point(1, 2)) # True', 'from dataclasses import dataclass, field\n\n@dataclass\nclass Cart:\n items: list = field(default_factory=list)\n tags: set = field(default_factory=set)\n metadata: dict = field(default_factory=dict)\n\na, b = Cart(), Cart()\na.items.append("apple")\nprint(a.items, b.items) # [\'apple\'] [] (each Cart has its own list)', 'import time\nfrom dataclasses import dataclass, field\n\n@dataclass\nclass User:\n username: str\n password_hash: str = field(repr=False) # not shown when you print a User\n created_at: float = field(default_factory=time.time, compare=False)\n cache: dict = field(default_factory=dict, repr=False, compare=False, init=False)\n\nu = User("surya", "$argon2id$...")\nprint(u) # User(username=\'surya\', created_at=<the current time>)', "@dataclass(slots=True)\nclass Point:\n x: float\n y: float\n\np = Point(1, 2)\n# p.z = 3 # AttributeError: 'Point' object has no attribute 'z'", "from dataclasses import asdict, astuple\n\n@dataclass\nclass Point:\n x: int\n y: int\n\np = Point(3, 4)\nprint(asdict(p)) # {'x': 3, 'y': 4}\nprint(astuple(p)) # (3, 4)", ] def _pm_run_earlier(_srcs, _g): import sys as _s, io as _io, warnings as _w for _src in _srcs: _o, _e = _s.stdout, _s.stderr _s.stdout = _s.stderr = _io.StringIO() try: with _w.catch_warnings(): _w.simplefilter('ignore') exec(compile(_src, '<earlier block>', 'exec'), _g) except (Exception, SystemExit): pass finally: _s.stdout, _s.stderr = _o, _e if 'matplotlib.pyplot' in _s.modules: _s.modules['matplotlib.pyplot'].close('all') # their plots belong to their own blocks _pm_run_earlier(_pm_earlier, globals()) del _pm_earlier, _pm_run_earlier
9. When to Use What
Python has several "bag of fields" tools. Quick comparison:
| Tool | Mutable? | Hashable? | Type-checked? | Best for |
|---|---|---|---|---|
@dataclass | Yes | No (by default) | Annotations only | Most cases: modelled data with optional methods |
@dataclass(frozen=True) | No | Yes | Annotations only | Value objects, dict keys, hashable records |
NamedTuple | No | Yes | Annotations only | Tuple-like records, indexable + unpackable |
TypedDict | Yes (it's a dict) | No | Static-check only | Wire formats: JSON shapes you don't want to convert |
Plain class | Yes | Identity only | Manual | When you need real behaviour, not just data |
Plain dict | Yes | No | None | Throwaway data, sets of unknown keys |
Rule of thumb: start with @dataclass. Switch to frozen=True when you want hashability or immutability. Use a plain dict when the keys are not known in advance. Use a full class when the behaviour matters more than the data.
Common Mistakes
1. default=[] instead of default_factory=list. @dataclass raises ValueError for a list, dict or set default (and, since Python 3.11, for any unhashable default). Always use field(default_factory=list) for mutable defaults.
2. Expecting frozen=True to deep-freeze.
@dataclass(frozen=True) class Box: items: list = field(default_factory=list) b = Box() b.items.append("a") # still works, because the list is not frozen # b.items = [] # FrozenInstanceError (frozen blocks assigning a new value)
setup added so this can run · re-runs 3 earlier blocks on this page first
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) def dataclass(*_a, **_kw): print('-> dataclass() called') return _AutoMock('dataclass()') def field(*_a, **_kw): print('-> field() called') return _AutoMock('field()') # Earlier blocks on this page that this one builds on, re-run quietly first. try: from dataclasses import dataclass from dataclasses import dataclass, field import time except Exception: pass _pm_earlier = [ 'from dataclasses import dataclass\n\n@dataclass\nclass Point:\n x: float\n y: float\n z: float = 0\n\np = Point(1, 2)\nprint(p) # Point(x=1, y=2, z=0)\nprint(p == Point(1, 2)) # True', 'from dataclasses import dataclass, field\n\n@dataclass\nclass Cart:\n items: list = field(default_factory=list)\n tags: set = field(default_factory=set)\n metadata: dict = field(default_factory=dict)\n\na, b = Cart(), Cart()\na.items.append("apple")\nprint(a.items, b.items) # [\'apple\'] [] (each Cart has its own list)', 'import time\nfrom dataclasses import dataclass, field\n\n@dataclass\nclass User:\n username: str\n password_hash: str = field(repr=False) # not shown when you print a User\n created_at: float = field(default_factory=time.time, compare=False)\n cache: dict = field(default_factory=dict, repr=False, compare=False, init=False)\n\nu = User("surya", "$argon2id$...")\nprint(u) # User(username=\'surya\', created_at=<the current time>)', ] def _pm_run_earlier(_srcs, _g): import sys as _s, io as _io, warnings as _w for _src in _srcs: _o, _e = _s.stdout, _s.stderr _s.stdout = _s.stderr = _io.StringIO() try: with _w.catch_warnings(): _w.simplefilter('ignore') exec(compile(_src, '<earlier block>', 'exec'), _g) except (Exception, SystemExit): pass finally: _s.stdout, _s.stderr = _o, _e if 'matplotlib.pyplot' in _s.modules: _s.modules['matplotlib.pyplot'].close('all') # their plots belong to their own blocks _pm_run_earlier(_pm_earlier, globals()) del _pm_earlier, _pm_run_earlier
frozen blocks attribute rebinding. The objects those attributes refer to can still change. Use tuples for nested immutability.
3. Custom __eq__ without eq=False.
@dataclass class Account: id: str balance: int def __eq__(self, other): # ⚠️ kept, but now Account can't be hashed return isinstance(other, Account) and self.id == other.id
setup added so this can run · re-runs 3 earlier blocks on this page first
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) dataclass = _AutoMock('dataclass') # Earlier blocks on this page that this one builds on, re-run quietly first. try: from dataclasses import dataclass from dataclasses import dataclass, field import time except Exception: pass _pm_earlier = [ 'from dataclasses import dataclass\n\n@dataclass\nclass Point:\n x: float\n y: float\n z: float = 0\n\np = Point(1, 2)\nprint(p) # Point(x=1, y=2, z=0)\nprint(p == Point(1, 2)) # True', 'from dataclasses import dataclass, field\n\n@dataclass\nclass Cart:\n items: list = field(default_factory=list)\n tags: set = field(default_factory=set)\n metadata: dict = field(default_factory=dict)\n\na, b = Cart(), Cart()\na.items.append("apple")\nprint(a.items, b.items) # [\'apple\'] [] (each Cart has its own list)', 'import time\nfrom dataclasses import dataclass, field\n\n@dataclass\nclass User:\n username: str\n password_hash: str = field(repr=False) # not shown when you print a User\n created_at: float = field(default_factory=time.time, compare=False)\n cache: dict = field(default_factory=dict, repr=False, compare=False, init=False)\n\nu = User("surya", "$argon2id$...")\nprint(u) # User(username=\'surya\', created_at=<the current time>)', ] def _pm_run_earlier(_srcs, _g): import sys as _s, io as _io, warnings as _w for _src in _srcs: _o, _e = _s.stdout, _s.stderr _s.stdout = _s.stderr = _io.StringIO() try: with _w.catch_warnings(): _w.simplefilter('ignore') exec(compile(_src, '<earlier block>', 'exec'), _g) except (Exception, SystemExit): pass finally: _s.stdout, _s.stderr = _o, _e if 'matplotlib.pyplot' in _s.modules: _s.modules['matplotlib.pyplot'].close('all') # their plots belong to their own blocks _pm_run_earlier(_pm_earlier, globals()) del _pm_earlier, _pm_run_earlier
@dataclass keeps your hand-written __eq__, but because eq=True is still the default it sets __hash__ to None, so putting an Account in a set or using it as a dict key raises TypeError: unhashable type. Pass eq=False and write __hash__ yourself:
@dataclass(eq=False) class Account: id: str balance: int def __eq__(self, other): return isinstance(other, Account) and self.id == other.id def __hash__(self): return hash(self.id)
setup added so this can run · re-runs 3 earlier blocks on this page first
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) def dataclass(*_a, **_kw): print('-> dataclass() called') return _AutoMock('dataclass()') # Earlier blocks on this page that this one builds on, re-run quietly first. try: from dataclasses import dataclass from dataclasses import dataclass, field import time except Exception: pass _pm_earlier = [ 'from dataclasses import dataclass\n\n@dataclass\nclass Point:\n x: float\n y: float\n z: float = 0\n\np = Point(1, 2)\nprint(p) # Point(x=1, y=2, z=0)\nprint(p == Point(1, 2)) # True', 'from dataclasses import dataclass, field\n\n@dataclass\nclass Cart:\n items: list = field(default_factory=list)\n tags: set = field(default_factory=set)\n metadata: dict = field(default_factory=dict)\n\na, b = Cart(), Cart()\na.items.append("apple")\nprint(a.items, b.items) # [\'apple\'] [] (each Cart has its own list)', 'import time\nfrom dataclasses import dataclass, field\n\n@dataclass\nclass User:\n username: str\n password_hash: str = field(repr=False) # not shown when you print a User\n created_at: float = field(default_factory=time.time, compare=False)\n cache: dict = field(default_factory=dict, repr=False, compare=False, init=False)\n\nu = User("surya", "$argon2id$...")\nprint(u) # User(username=\'surya\', created_at=<the current time>)', ] def _pm_run_earlier(_srcs, _g): import sys as _s, io as _io, warnings as _w for _src in _srcs: _o, _e = _s.stdout, _s.stderr _s.stdout = _s.stderr = _io.StringIO() try: with _w.catch_warnings(): _w.simplefilter('ignore') exec(compile(_src, '<earlier block>', 'exec'), _g) except (Exception, SystemExit): pass finally: _s.stdout, _s.stderr = _o, _e if 'matplotlib.pyplot' in _s.modules: _s.modules['matplotlib.pyplot'].close('all') # their plots belong to their own blocks _pm_run_earlier(_pm_earlier, globals()) del _pm_earlier, _pm_run_earlier
4. Comparing dataclasses of different types is always False. The generated __eq__ checks that both objects have the same class first. A Point and a Pixel with the same (x, y) are not equal. This is often what you want, but you should know it.
5. Inheriting from a dataclass with defaults. Once a parent field has a default, every child field must also have one. This is the same rule as for function parameters with defaults. You will meet this error whenever the parent has even one default.
🎯 Your Turn: Building an Order
Build an Order dataclass that models a shopping order with line items, validation, and a nested frozen Address. Requirements:
1. A frozen Address dataclass with street, city, postcode. It must be hashable so it can go in a set.
2. A LineItem dataclass with name, quantity (int), unit_price (float).
3. An Order dataclass with order_id, shipping_address (an Address), and items (a list of LineItem, defaulting to empty).
4. __post_init__ on Order validates that every line item has quantity > 0.
5. A total @property on Order returning the sum of quantity * unit_price across all items.
6. An add(item) method that appends a LineItem, re-validating quantity.
Skeleton:
from dataclasses import dataclass, field from typing import List @dataclass(frozen=True) class Address: # TODO 1: street, city and postcode, all of type str ... @dataclass class LineItem: # TODO 2 ... @dataclass class Order: order_id: str shipping_address: Address items: List[LineItem] = field(default_factory=list) def __post_init__(self): # TODO 3: raise ValueError if any item has quantity <= 0 ... @property def total(self): # TODO 4 ... def add(self, item): # TODO 5: check the item's quantity, then append it ... addr = Address("221B Baker St", "London", "NW1 6XE") order = Order("ORD-001", addr, [ LineItem("widget", 3, 9.99), LineItem("gadget", 1, 49.50), ]) print(order.total) # 79.47 order.add(LineItem("gizmo", 2, 5.00)) print(order.total) # 89.47 # Address is hashable, so it can be a dict key warehouses = {addr: "London hub"} print(warehouses[Address("221B Baker St", "London", "NW1 6XE")])
Hint 1: Frozen + hashable
@dataclass(frozen=True) on Address gives you both immutability and a working __hash__ so two addresses with the same fields hash identically.
Hint 2: Validating in __post_init__
Loop over self.items and raise ValueError if any item.quantity <= 0. Use the same check inside add before appending, or factor it out into a small helper method.
Show full solution
from dataclasses import dataclass, field from typing import List @dataclass(frozen=True) class Address: street: str city: str postcode: str @dataclass class LineItem: name: str quantity: int unit_price: float @property def subtotal(self): return self.quantity * self.unit_price @dataclass class Order: order_id: str shipping_address: Address items: List[LineItem] = field(default_factory=list) def __post_init__(self): for item in self.items: self._validate(item) @staticmethod def _validate(item): if item.quantity <= 0: raise ValueError(f"quantity must be positive, got {item.quantity} for {item.name!r}") @property def total(self): return sum(item.subtotal for item in self.items) def add(self, item): self._validate(item) self.items.append(item) addr = Address("221B Baker St", "London", "NW1 6XE") order = Order("ORD-001", addr, [ LineItem("widget", 3, 9.99), LineItem("gadget", 1, 49.50), ]) print(f"initial total: {order.total:.2f}") order.add(LineItem("gizmo", 2, 5.00)) print(f"after add: {order.total:.2f}") # Try a bad one try: order.add(LineItem("broken", 0, 100)) except ValueError as e: print(f"refused: {e}") # Address works as a dict key, because frozen=True makes it hashable warehouses = {addr: "London hub"} print(warehouses[Address("221B Baker St", "London", "NW1 6XE")])
Three dataclasses in about 30 lines give you validation, a derived total, hashable nested objects, and clear error messages on bad input. With plain classes, you would write much more code.
What You Learned
@dataclassgenerates__init__,__repr__, and__eq__from annotated fields. Type hints are mandatory.field(default_factory=...)for mutable defaults. The cause is the same as the mutable default argument bug.frozen=Truemakes a dataclass immutable and hashable. It is shallow: mutable objects inside it can still change.order=Trueauto-generates comparison operators.field(compare=False)to exclude individual fields.__post_init__runs validation or computes derived fields after the generated__init__.field()controls per-attributeinit,repr,compare,hashbehaviour.slots=Truesaves memory and blocks new attributes, for classes you create in very large numbers.asdict/astupleconvert a dataclass to a dict or tuple, for example beforejson.dumps.
Next: later in this Intermediate path you will learn decorators and generators, two more tools you will see often in real Python code.
Practice this
on practicepython.inShort exercises that run in your browser and tell you what your code did, as well as whether a test passed.
Found this useful? Share it with a friend who’s learning Python.