Python · Syntax · Intermediate

dataclasses

10 tasks

Learn how to use Python dataclasses to define clean, boilerplate-free data containers with automatic __init__, __repr__, and __eq__ generation.

@dataclass: Automatic Boilerplate

#
Before dataclasses, creating a simple data container in Python required writing `__init__`, `__repr__`, and `__eq__` manually — a lot of repetitive code. The `@dataclass` decorator (Python 3.7+) generates all of this automatically. ```python from dataclasses import dataclass, field ``` ## The problem without dataclasses ```python # Old way: lots of boilerplate class Point: def __init__(self, x, y): self.x = x self.y = y def __repr__(self): return f"Point(x={self.x}, y={self.y})" def __eq__(self, other): return isinstance(other, Point) and self.x == other.x and self.y == other.y ``` ## The dataclass solution ```python @dataclass class Point: x: float y: float p1 = Point(1.0, 2.0) p2 = Point(1.0, 2.0) print(p1) # Point(x=1.0, y=2.0) — __repr__ auto-generated print(p1 == p2) # True — __eq__ auto-generated print(p1.x) # 1.0 ``` The decorator generates `__init__`, `__repr__`, and `__eq__` based on the class annotations. ## Default values ```python @dataclass class Config: host: str = "localhost" port: int = 8080 debug: bool = False c = Config() # all defaults print(c) # Config(host='localhost', port=8080, debug=False) c2 = Config(host="0.0.0.0", port=443) print(c2) # Config(host='0.0.0.0', port=443, debug=False) ``` **Rule**: fields with defaults must come after fields without defaults (same rule as function parameters). ## field() for mutable defaults You cannot use `default=[]` directly — Python would share the same list between all instances. Use `field(default_factory=list)` instead: ```python @dataclass class Student: name: str grades: list = field(default_factory=list) # each instance gets its own list active: bool = True s1 = Student("Alice") s2 = Student("Bob") s1.grades.append(95) print(s1.grades) # [95] print(s2.grades) # [] — independent! ``` ## __post_init__: computed fields and validation ```python @dataclass class Rectangle: width: float height: float area: float = field(init=False) # excluded from __init__ 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.0, 5.0) print(r) # Rectangle(width=4.0, height=5.0, area=20.0) print(r.area) # 20.0 ``` `__post_init__` runs automatically after `__init__`.

Frozen, Order & Inheritance

#
## frozen=True: immutable dataclasses ```python from dataclasses import dataclass @dataclass(frozen=True) class Color: r: int g: int b: int red = Color(255, 0, 0) print(red) # Color(r=255, g=0, b=0) # Frozen instances are hashable — can be used as dict keys or in sets! palette = {red, Color(0, 255, 0), Color(0, 0, 255)} config = {Color(255, 0, 0): "red", Color(0, 0, 255): "blue"} # Attempting to modify raises FrozenInstanceError: red.r = 100 # FrozenInstanceError: cannot assign to field 'r' ``` ## order=True: comparison support ```python @dataclass(order=True) class Version: major: int minor: int patch: int v1 = Version(1, 2, 3) v2 = Version(1, 3, 0) v3 = Version(2, 0, 0) print(v1 < v2) # True — compares field by field print(v2 < v3) # True versions = [v3, v1, v2] print(sorted(versions)) # [Version(major=1, minor=2, patch=3), Version(major=1, minor=3, patch=0), Version(major=2, minor=0, patch=0)] ``` ## Inheritance ```python @dataclass class Animal: name: str sound: str @dataclass class Dog(Animal): breed: str sound: str = "woof" # override default d = Dog(name="Rex", breed="Labrador") print(d) # Dog(name='Rex', sound='woof', breed='Labrador') print(d.sound) # woof ``` ## Nested dataclasses ```python @dataclass class Address: street: str city: str country: str = "Ukraine" @dataclass class Person: name: str age: int address: Address p = Person( name="Alice", age=30, address=Address("Khreschatyk St", "Kyiv"), ) print(p) # Person(name='Alice', age=30, address=Address(street='Khreschatyk St', city='Kyiv', country='Ukraine')) print(p.address.city) # Kyiv ``` ## Converting to dict and tuple ```python from dataclasses import dataclass, asdict, astuple @dataclass class Point: x: float y: float p = Point(1.5, 2.5) d = asdict(p) # {"x": 1.5, "y": 2.5} t = astuple(p) # (1.5, 2.5) # Useful for JSON serialization import json json_str = json.dumps(asdict(p)) print(json_str) # {"x": 1.5, "y": 2.5} ```

dataclasses Reference

#
## @dataclass parameters ```python @dataclass( init=True, # generate __init__ repr=True, # generate __repr__ eq=True, # generate __eq__ and __ne__ order=False, # generate __lt__, __le__, __gt__, __ge__ unsafe_hash=False, # generate __hash__ even if eq=True frozen=False, # make instances immutable slots=False, # use __slots__ (Python 3.10+) kw_only=False, # all fields keyword-only in __init__ (Python 3.10+) ) class MyClass: ... ``` ## field() parameters ```python from dataclasses import field @dataclass class Example: # field(default, default_factory, init, repr, hash, compare, metadata) name: str # required field score: float = 0.0 # simple default tags: list = field(default_factory=list) # mutable default _id: int = field(init=False, repr=False) # excluded from init and repr label: str = field(default="n/a", compare=False) # excluded from eq ``` | Parameter | Default | Meaning | |---|---|---| | `default` | MISSING | Default value | | `default_factory` | MISSING | Callable that produces default | | `init` | True | Include in `__init__` | | `repr` | True | Include in `__repr__` | | `hash` | None | Include in `__hash__` | | `compare` | True | Include in `__eq__` and ordering | | `metadata` | None | Arbitrary read-only metadata dict | ## Helper functions | Function | What it does | |---|---| | `asdict(obj)` | Convert to nested dict | | `astuple(obj)` | Convert to nested tuple | | `fields(class_or_instance)` | Return tuple of Field objects | | `replace(obj, **changes)` | Return new instance with some fields changed | ## dataclass vs regular class vs namedtuple | Feature | regular class | namedtuple | dataclass | |---|---|---|---| | Auto `__init__` | No | Yes | Yes | | Auto `__repr__` | No | Yes | Yes | | Auto `__eq__` | No | Yes | Yes | | Mutable | Yes | No | Yes (default) | | Hashable | No | Yes | With frozen=True | | Ordering | Manual | Yes | With order=True | | Methods | Yes | Limited | Yes | | Type hints | Optional | Optional | Required | ## Common patterns ```python from dataclasses import dataclass, field, replace # Immutable value object @dataclass(frozen=True) class Money: amount: float currency: str = "USD" # Create modified copy m1 = Money(10.0) m2 = replace(m1, amount=20.0) # Money(amount=20.0, currency='USD') # Computed field @dataclass class Circle: radius: float area: float = field(init=False) def __post_init__(self): import math self.area = math.pi * self.radius ** 2 # Convert for JSON import json from dataclasses import asdict data = json.dumps(asdict(some_dataclass_instance)) ```
01

Basic Dataclass

#

Create a dataclass `Book` with fields: `title: str`, `author: str`, `year: int`, `rating: float = 0.0`. Then create two instances: `Book("1984", "Orwell", 1949, 9.5)` and `Book("Dune", "Herbert", 1965)`. Print both and verify that `==` works (create two identical Book objects and check they are equal).

from dataclasses import dataclass


@dataclass
class Book:
    pass


b1 = Book("1984", "Orwell", 1949, 9.5)
b2 = Book("Dune", "Herbert", 1965)
print(b1)
print(b2)

# Verify equality
b3 = Book("1984", "Orwell", 1949, 9.5)
print(b1 == b3)   # True
Solution
from dataclasses import dataclass


@dataclass
class Book:
    title: str
    author: str
    year: int
    rating: float = 0.0


b1 = Book("1984", "Orwell", 1949, 9.5)
b2 = Book("Dune", "Herbert", 1965)    # rating defaults to 0.0
print(b1)   # Book(title="1984", author="Orwell", year=1949, rating=9.5)
print(b2)   # Book(title="Dune", author="Herbert", year=1965, rating=0.0)

b3 = Book("1984", "Orwell", 1949, 9.5)
print(b1 == b3)   # True
02

Mutable Default with field()

#

Create a dataclass `Team` with fields `name: str` and `members: list` where `members` defaults to an empty list. Show that each instance gets its own independent list by creating two teams, adding a member to the first, and verifying the second is still empty. Use `field(default_factory=list)`.

from dataclasses import dataclass, field


@dataclass
class Team:
    name: str
    members: list = ???   # fix this


t1 = Team("Alpha")
t2 = Team("Beta")
t1.members.append("Alice")
print(t1.members)   # ["Alice"]
print(t2.members)   # []  <- must be empty
Solution
from dataclasses import dataclass, field


@dataclass
class Team:
    name: str
    members: list = field(default_factory=list)


t1 = Team("Alpha")
t2 = Team("Beta")
t1.members.append("Alice")
print(t1.members)   # ["Alice"]
print(t2.members)   # []
03

__post_init__ Validation

#

Create a dataclass `Temperature` with a single field `celsius: float`. In `__post_init__`, validate that the temperature is above absolute zero (-273.15°C) and raise `ValueError` with message `"Temperature below absolute zero"` if not. Test both valid and invalid cases.

from dataclasses import dataclass


@dataclass
class Temperature:
    celsius: float

    def __post_init__(self):
        pass   # validate here


print(Temperature(100))    # Temperature(celsius=100)
print(Temperature(-273))   # Temperature(celsius=-273)
Temperature(-300)          # raises ValueError
Solution
from dataclasses import dataclass


@dataclass
class Temperature:
    celsius: float

    def __post_init__(self):
        if self.celsius < -273.15:
            raise ValueError("Temperature below absolute zero")


print(Temperature(100))    # Temperature(celsius=100)
print(Temperature(-273))   # Temperature(celsius=-273)
try:
    Temperature(-300)
except ValueError as e:
    print(e)   # Temperature below absolute zero
04

Computed Field

#

Create a dataclass `Circle` with field `radius: float` and a computed field `area: float` that is NOT part of `__init__` (use `field(init=False)`). Calculate `area` in `__post_init__` as `math.pi * radius ** 2`. Print a Circle and show that `area` is included in the repr.

from dataclasses import dataclass, field
import math


@dataclass
class Circle:
    radius: float
    area: float = field(init=False)

    def __post_init__(self):
        pass   # compute area here


c = Circle(5)
print(c)        # Circle(radius=5, area=78.539...)
print(c.area)   # 78.539...
Solution
from dataclasses import dataclass, field
import math


@dataclass
class Circle:
    radius: float
    area: float = field(init=False)

    def __post_init__(self):
        self.area = math.pi * self.radius ** 2


c = Circle(5)
print(c)        # Circle(radius=5, area=78.53981633974483)
print(c.area)   # 78.53981633974483
05

Frozen Dataclass as Dict Key

#

Create a frozen dataclass `Coordinate` with fields `lat: float` and `lon: float`. Demonstrate that: 1. You cannot modify a `Coordinate` instance (show the error) 2. `Coordinate` instances can be used as dictionary keys — create a dict mapping coordinates to location names.

from dataclasses import dataclass


@dataclass(frozen=???)
class Coordinate:
    lat: float
    lon: float


c = Coordinate(50.45, 30.52)

# 1. Try to modify (should raise FrozenInstanceError)
try:
    c.lat = 0
except Exception as e:
    print(e)

# 2. Use as dict key
locations = {c: "Kyiv"}
print(locations[Coordinate(50.45, 30.52)])   # Kyiv
Solution
from dataclasses import dataclass


@dataclass(frozen=True)
class Coordinate:
    lat: float
    lon: float


c = Coordinate(50.45, 30.52)

try:
    c.lat = 0
except Exception as e:
    print(type(e).__name__, e)   # FrozenInstanceError

locations = {c: "Kyiv"}
print(locations[Coordinate(50.45, 30.52)])   # Kyiv
06

Sortable Dataclass

#

Create a dataclass `Student` with `order=True` and fields: `grade: float`, `name: str`. Create a list of 5 students with different grades and names, sort it (the primary sort should be by `grade`), and print the result. Note: ordering compares fields in declaration order — put `grade` first.

from dataclasses import dataclass


@dataclass(order=???)
class Student:
    grade: float
    name: str


students = [
    Student(3.5, "Alice"),
    Student(4.0, "Bob"),
    Student(3.5, "Charlie"),
    Student(2.8, "Dave"),
    Student(4.0, "Eve"),
]
print(sorted(students))
Solution
from dataclasses import dataclass


@dataclass(order=True)
class Student:
    grade: float
    name: str


students = [
    Student(3.5, "Alice"),
    Student(4.0, "Bob"),
    Student(3.5, "Charlie"),
    Student(2.8, "Dave"),
    Student(4.0, "Eve"),
]
for s in sorted(students):
    print(s)
# Student(grade=2.8, name="Dave")
# Student(grade=3.5, name="Alice")
# Student(grade=3.5, name="Charlie")
# Student(grade=4.0, name="Bob")
# Student(grade=4.0, name="Eve")
07

Nested Dataclasses

#

Create two dataclasses: `Address` (fields: `street: str`, `city: str`, `country: str = "Ukraine"`) and `Person` (fields: `name: str`, `age: int`, `address: Address`). Create a `Person` instance with a nested `Address`, print it, and access the nested city. Then use `asdict()` to convert the person to a dictionary.

from dataclasses import dataclass, asdict


@dataclass
class Address:
    street: str
    city: str
    country: str = "Ukraine"


@dataclass
class Person:
    name: str
    age: int
    address: Address


p = Person("Alice", 30, Address("Main St", "Kyiv"))
print(p)
print(p.address.city)
print(asdict(p))
Solution
from dataclasses import dataclass, asdict


@dataclass
class Address:
    street: str
    city: str
    country: str = "Ukraine"


@dataclass
class Person:
    name: str
    age: int
    address: Address


p = Person("Alice", 30, Address("Main St", "Kyiv"))
print(p)
# Person(name="Alice", age=30, address=Address(street="Main St", city="Kyiv", country="Ukraine"))
print(p.address.city)   # Kyiv
print(asdict(p))
# {"name": "Alice", "age": 30, "address": {"street": "Main St", "city": "Kyiv", "country": "Ukraine"}}
08

replace() to Create Variants

#

Create a frozen dataclass `Config` with fields `host: str = "localhost"`, `port: int = 8080`, `debug: bool = False`. Use `dataclasses.replace()` to create a production config from a development config by changing `host` and `debug`. Show that the original is unchanged and the new instance has the updated values.

from dataclasses import dataclass, replace


@dataclass(frozen=True)
class Config:
    host: str = "localhost"
    port: int = 8080
    debug: bool = False


dev = Config(debug=True)
prod = replace(dev, ???)   # change host and debug
print(dev)
print(prod)
print(dev is prod)   # False
Solution
from dataclasses import dataclass, replace


@dataclass(frozen=True)
class Config:
    host: str = "localhost"
    port: int = 8080
    debug: bool = False


dev = Config(debug=True)
prod = replace(dev, host="prod.example.com", debug=False)
print(dev)    # Config(host="localhost", port=8080, debug=True)
print(prod)   # Config(host="prod.example.com", port=8080, debug=False)
print(dev is prod)   # False — different objects
09

Dataclass to JSON

#

Create a dataclass `Product` with fields `name: str`, `price: float`, `in_stock: bool = True`, and `tags: list = field(default_factory=list)`. Create 3 product instances, convert each to a dict using `asdict()`, and then serialize the list of dicts to a JSON string using `json.dumps()`. Print the result.

from dataclasses import dataclass, field, asdict
import json


@dataclass
class Product:
    name: str
    price: float
    in_stock: bool = True
    tags: list = field(default_factory=list)


products = [
    Product("Apple", 0.99, tags=["fruit", "fresh"]),
    Product("Laptop", 999.0, tags=["electronics"]),
    Product("Pen", 1.5, in_stock=False),
]
# Convert to JSON and print
Solution
from dataclasses import dataclass, field, asdict
import json


@dataclass
class Product:
    name: str
    price: float
    in_stock: bool = True
    tags: list = field(default_factory=list)


products = [
    Product("Apple", 0.99, tags=["fruit", "fresh"]),
    Product("Laptop", 999.0, tags=["electronics"]),
    Product("Pen", 1.5, in_stock=False),
]
data = [asdict(p) for p in products]
print(json.dumps(data, indent=2))
10

Inventory System

#

Design a small inventory system using dataclasses: - `Category` (frozen, fields: `name: str`, `tax_rate: float = 0.0`) - `Item` (fields: `name: str`, `price: float`, `quantity: int`, `category: Category`) - `Inventory` (field: `items: list = field(default_factory=list)`, method `add_item(item)`, method `total_value()` that returns sum of price*quantity for all items) Create a few items and print the total inventory value.

from dataclasses import dataclass, field


@dataclass(frozen=True)
class Category:
    name: str
    tax_rate: float = 0.0


@dataclass
class Item:
    name: str
    price: float
    quantity: int
    category: Category


@dataclass
class Inventory:
    items: list = field(default_factory=list)

    def add_item(self, item):
        pass

    def total_value(self):
        pass


inv = Inventory()
food = Category("Food", 0.05)
tech = Category("Tech", 0.2)
inv.add_item(Item("Apple", 1.0, 100, food))
inv.add_item(Item("Laptop", 800.0, 5, tech))
print(inv.total_value())   # 4100.0
Solution
from dataclasses import dataclass, field


@dataclass(frozen=True)
class Category:
    name: str
    tax_rate: float = 0.0


@dataclass
class Item:
    name: str
    price: float
    quantity: int
    category: Category


@dataclass
class Inventory:
    items: list = field(default_factory=list)

    def add_item(self, item):
        self.items.append(item)

    def total_value(self):
        return sum(item.price * item.quantity for item in self.items)


inv = Inventory()
food = Category("Food", 0.05)
tech = Category("Tech", 0.2)
inv.add_item(Item("Apple", 1.0, 100, food))
inv.add_item(Item("Laptop", 800.0, 5, tech))
print(inv.total_value())   # 4100.0