Python · Syntax · Intermediate
dataclasses
Learn how to use Python dataclasses to define clean, boilerplate-free data containers with automatic __init__, __repr__, and __eq__ generation.
Quick topic start and explanations before exercises (exercises below):
Frozen, Order & Inheritance
#dataclasses Reference
#Exercises:
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
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) # []
__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
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
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
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")
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"}}
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
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))
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