Mathew K Analytics

Lesson 18 · Python standard library deep dive

Python dataclasses Explained: Cleaner Classes, Less Code | Standard Library #18

Video eighteen of the twenty-five-part series: dataclasses, for cutting way down on class boilerplate. Auto-generated init/repr/eq, defaults, frozen…

⬇ Download notebookOpen in Colab ↗
dataclasses

What you'll learn

Data

No separate download needed — the notebook creates or downloads everything it uses.

📓 Full notebook

Download .ipynb

Python Standard Library Deep-Dive, Video 18: dataclasses#

  • Video eighteen of the twenty-five-part series: dataclasses, for cutting way down on class boilerplate.
  • Auto-generated init/repr/eq, defaults, frozen immutability, ordering, validation, and inheritance.
  • Let's get into it.

Part 1: The Boilerplate Problem#

class PlainPoint:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    def __repr__(self):
        return f'PlainPoint(x={self.x}, y={self.y})'
    def __eq__(self, other):
        return (self.x, self.y) == (other.x, other.y)
p1 = PlainPoint(1, 2)
p2 = PlainPoint(1, 2)
print(p1)
print(p1 == p2)
PlainPoint(x=1, y=2)
True

Part 2: @dataclass Basics#

from dataclasses import dataclass
@dataclass
class Point:
    x: int
    y: int
p1 = Point(1, 2)
p2 = Point(1, 2)
print(p1)
print(p1 == p2)
print(p1 is p2)
Point(x=1, y=2)
True
False

Part 3: Default Values and default_factory#

from dataclasses import field
@dataclass
class Task:
    title: str
    done: bool = False
    tags: list = field(default_factory=list)
t1 = Task('write lesson')
t2 = Task('review lesson')
t1.tags.append('urgent')
print(t1.tags)
print(t2.tags)
['urgent']
[]

Part 4: Type Hints Are Not Enforced at Runtime#

@dataclass
class Item:
    name: str
    price: float
wrong_types = Item(name=123, price='not a number')
print(wrong_types)
print(type(wrong_types.name))
Item(name=123, price='not a number')
<class 'int'>

Part 5: frozen=True - Immutability#

@dataclass(frozen=True)
class FrozenPoint:
    x: int
    y: int
fp = FrozenPoint(3, 4)
print(fp)
try:
    fp.x = 99
except Exception as e:
    print(f'{type(e).__name__}: {e}')
FrozenPoint(x=3, y=4)
FrozenInstanceError: cannot assign to field 'x'
points = {FrozenPoint(0, 0): 'origin', FrozenPoint(3, 4): 'other'}
print(points[FrozenPoint(0, 0)])
origin

Part 6: order=True - Comparison and Sorting#

@dataclass(order=True)
class Version:
    major: int
    minor: int
    patch: int
versions = [Version(1, 2, 0), Version(1, 0, 5), Version(2, 0, 0), Version(1, 2, 3)]
print(sorted(versions))
print(Version(1, 0, 0) < Version(1, 0, 1))
[Version(major=1, minor=0, patch=5), Version(major=1, minor=2, patch=0), Version(major=1, minor=2, patch=3), Version(major=2, minor=0, patch=0)]
True

Part 7: post_init - Validation and Derived Fields#

@dataclass
class Rectangle:
    width: float
    height: float
    area: float = field(init=False)
    def __post_init__(self):
        if self.width <= 0 or self.height <= 0:
            raise ValueError('width and height must be positive')
        self.area = self.width * self.height
r = Rectangle(4, 5)
print(r)
try:
    Rectangle(-1, 5)
except ValueError as e:
    print(f'caught: {e}')
Rectangle(width=4, height=5, area=20)
caught: width and height must be positive

Part 8: field() Options - repr, compare, and metadata#

@dataclass
class User:
    username: str
    password_hash: str = field(repr=False)
    login_count: int = field(default=0, compare=False)
u1 = User('ana', 'abc123hash')
u2 = User('ana', 'abc123hash')
u2.login_count = 50
print(u1)
print(u1 == u2)
User(username='ana', login_count=0)
True

Part 9: Inheritance with dataclasses#

@dataclass
class Animal:
    name: str
    species: str
@dataclass
class Pet(Animal):
    owner: str
    nickname: str = 'buddy'
p = Pet(name='Rex', species='dog', owner='Ana')
print(p)
Pet(name='Rex', species='dog', owner='Ana', nickname='buddy')

Part 10: asdict/astuple and a Real Config Pattern#

from dataclasses import asdict, astuple
@dataclass
class AppConfig:
    debug: bool = False
    max_connections: int = 10
    allowed_hosts: list = field(default_factory=lambda: ['localhost'])
    def __post_init__(self):
        if self.max_connections <= 0:
            raise ValueError('max_connections must be positive')
config = AppConfig(debug=True, max_connections=50)
print(config)
print(asdict(config))
print(astuple(config))
AppConfig(debug=True, max_connections=50, allowed_hosts=['localhost'])
{'debug': True, 'max_connections': 50, 'allowed_hosts': ['localhost']}
(True, 50, ['localhost'])

Wrap-Up: What You Learned#

  • @dataclass auto-generates init, repr, and eq from type-annotated class fields.
  • Plain defaults work like normal; mutable defaults need field(default_factory=...) to avoid sharing across instances.
  • Type annotations are documentation only; Python never enforces them at runtime.
  • frozen=True makes fields read-only and makes instances hashable, usable as dict keys.
  • order=True generates comparison methods, comparing fields in declared order like tuples.
  • post_init runs right after generated init, ideal for validation or computing derived fields.
  • field() also supports repr=False, compare=False, and metadata for fine-grained control.
  • dataclasses support inheritance; required fields must still precede defaulted ones across the full set.
  • asdict/astuple convert an instance, recursively, into a plain dict or tuple.
  • A real pattern: a validated, typed application config object built once at startup.
  • That wraps up dataclasses. Next up: typing and enum, for type hints and enumerations.

Found this useful?

All lessons, notebooks and datasets here are free. If they helped you, a coffee keeps new lessons coming.