Skip to content

Expert · Object-oriented programming · topic 39 of 50 · 30 min

Dataclasses and properties

Explanation

A dataclass is a class mostly used to hold data. Put @dataclass above it and list the fields with types; Python writes __init__, __repr__ and __eq__ for you. You can add defaults, make objects unchangeable with frozen=True, sortable with order=True, and use @property for computed values and validation.

The boring part of classes

Many classes just hold data: a student, a product, a train ticket. For each one you write __init__ that copies every argument to self, a __repr__ for printing, and an __eq__ for comparing. That is a lot of repeated typing. The dataclasses module writes it for you.

Think of it like this: A dataclass is like a printed bill book at a shop. The columns (item, quantity, price) are already printed; the shopkeeper only fills in the values. You only list the fields; Python prints the rest of the form for you.
python
from dataclasses import dataclass

@dataclass
class Product:
    name: str
    price: int
    qty: int = 1          # a default value

p = Product("Rice 5kg", 320, 2)
print(p)                               # Product(name='Rice 5kg', price=320, qty=2)
print(p == Product("Rice 5kg", 320, 2))  # True
print(Product("Salt", 25))             # qty defaults to 1
Fields are written as name: type. You get __init__, __repr__ and __eq__ for free.

@dataclass is a decorator: a line starting with @ placed above a class or function that adds extra behaviour to it. The types after the colons (str, int) are type hints. Python does not check them at run time, but they document your intent and help your editor find mistakes.

Careful: Fields with defaults must come after fields without defaults. Writing qty: int = 1 before price: int raises TypeError: non-default argument follows default argument.

Lists as defaults: field(default_factory=list)

python
from dataclasses import dataclass, field

@dataclass
class Cart:
    owner: str
    items: list = field(default_factory=list)   # a NEW list for every cart

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

a = Cart("Asha")
b = Cart("Ravi")
a.add("Milk")
print(a.items, b.items)   # ['Milk'] []
default_factory is a function called to make a fresh default for each object.

Why not items: list = []? A default value is created once and would be shared by every object, the same trap you saw with class attributes. Dataclasses protect you: writing = [] raises ValueError and tells you to use default_factory.

frozen=True and order=True

python
from dataclasses import dataclass

@dataclass(frozen=True)
class Station:
    code: str
    name: str

ndls = Station("NDLS", "New Delhi")
try:
    ndls.code = "BCT"
except Exception as e:
    print(type(e).__name__)      # FrozenInstanceError
print({ndls})                    # frozen dataclasses can go in a set

@dataclass(order=True)
class Rank:
    score: int
    name: str

print(sorted([Rank(88, "Om"), Rank(95, "Ira"), Rank(88, "Ali")]))
order=True compares fields in order: score first, then name.
OptionWhat it doesUse it when
frozen=TrueFields cannot be changed after creation; objects become hashableValues that should never change: a station code, a date range
order=TrueAdds <, <=, >, >= comparing fields like a tupleYou want to sort objects by their fields
field(default_factory=list)A fresh list (or dict) for each objectA field that starts empty and grows

@property: computed values and validation

A property looks like an attribute but runs a method. Use it for a value that is calculated from other fields, so it is never out of date. A property can also have a setter, which runs when you assign to it, so you can check the new value before storing it.

python
from dataclasses import dataclass

@dataclass
class CartItem:
    name: str
    price: int
    qty: int

    @property
    def total(self):           # computed every time, never stale
        return self.price * self.qty

item = CartItem("Atta 10kg", 450, 2)
print(item.total)   # 900 (no brackets)
item.qty = 3
print(item.total)   # 1350

class Product:
    def __init__(self, name, price):
        self.name = name
        self.price = price     # goes through the setter below

    @property
    def price(self):
        return self._price

    @price.setter
    def price(self, value):
        if value < 0:
            raise ValueError("Price cannot be negative")
        self._price = value

p = Product("Soap", 40)
try:
    p.price = -5
except ValueError as e:
    print(e)
The real value lives in self._price; the underscore means "internal, do not touch".
Tip: In a dataclass, you can check values in a method named __post_init__(self). The generated __init__ calls it right after setting the fields, so it is a good place to raise ValueError for bad data.

When to use a dataclass

  • Use a dataclass when the class is mainly a bundle of named values, with maybe a few small methods.
  • Use a normal class when the object is mostly behaviour with careful rules, like a bank account.
  • Use a plain dictionary for quick, one-off data, such as a row you just read from a CSV file.

Watch the lesson

Dataclasses and properties · 1/7

Dataclasses

In this lesson you will learn dataclasses, a quick way to write classes that mainly hold data.

Prefer a video by a teacher? Find free videos on “python dataclasses tutorial field default_factory property” on YouTube ↗

In real life: Train tickets in a booking app

A railway booking app has millions of tickets. Each ticket is a dataclass with a PNR, a passenger, a train number, a class and a fare. The station codes are frozen dataclasses because NDLS must never change. A @property called is_confirmed reads the status, so the screen and the SMS always agree.

python
from dataclasses import dataclass

@dataclass
class Ticket:
    pnr: str
    passenger: str
    train: str
    status: str = "WL 12"

    @property
    def is_confirmed(self):
        return self.status.startswith("CNF")

t = Ticket("4521367890", "Passenger A", "12951")
print(t, t.is_confirmed)
t.status = "CNF B2 34"
print(t.is_confirmed)
A dataclass with a default and a computed property.

Practice (0/3 done)

Task 1 · Code · Python

Make Item a dataclass with fields name (str), price (int) and qty (int, default 1). Add a method total(self) that returns price times qty.

Task 2 · Code · Python

Make Cart a dataclass with a field owner (str) and a field items (a list) that starts empty for EACH cart. Add a method add(self, item) that appends to items.

Task 3 · Code · Python

Write a class Product with __init__(self, name, price). Make price a @property with a setter that raises ValueError if the new price is negative. Add a read-only property price_with_gst that returns price * 1.18 rounded to 2 decimals.

Quick quiz

Question 1

1.Which methods does @dataclass write for you by default?

Question 2

2.Why use field(default_factory=list) instead of items: list = []?

Question 3

3.What happens when you change a field of a frozen=True dataclass?

Question 4

4.With @dataclass(order=True) and fields score then name, how are objects sorted?

Question 5

5.How do you read a @property called total on an object item?

Read more (free)

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

← Previous: Dunder (magic) methods