Dataclasses & Type Hints

The Course
January 10, 2026
5 min read

Most classes exist mainly to hold a few values. Writing __init__, __repr__ and __eq__ for each one by hand is exactly the sort of work a language should do for you.

The boilerplate problem

Twenty lines to hold three values Python
class Book:
    def __init__(self, title, author, pages=0):
        self.title = title
        self.author = author
        self.pages = pages

    def __repr__(self):
        return f"Book(title={self.title!r}, author={self.author!r}, pages={self.pages})"

    def __eq__(self, other):
        return (self.title, self.author, self.pages) == (other.title, other.author, other.pages)

book = Book("Dune", "Herbert", 412)
print(book)
print(book == Book("Dune", "Herbert", 412))

The same class as a dataclass

Five lines instead Python
from dataclasses import dataclass

@dataclass
class Book:
    title: str
    author: str
    pages: int = 0

book = Book("Dune", "Herbert", 412)
print(book)                                    # __repr__ for free
print(book == Book("Dune", "Herbert", 412))    # __eq__ for free
print(book.title, book.pages)

book.pages = 500                               # still an ordinary object
print(book)

The @dataclass line is a decorator: a function that takes your class and returns an enhanced version of it. It writes __init__, __repr__ and __eq__ from the annotated fields.

Dataclass options

Frozen, ordered, and computed fields Python
from dataclasses import dataclass, field

@dataclass(frozen=True, order=True)
class Version:
    major: int
    minor: int
    patch: int = 0

    def __str__(self):
        return f"{self.major}.{self.minor}.{self.patch}"

v1 = Version(1, 4)
v2 = Version(1, 10)

print(str(v1), str(v2))
print(v1 < v2)                 # order=True gives comparison from field order
print(sorted([v2, v1]))

v1.major = 2                   # frozen=True makes it immutable

frozen=True makes instances immutable and hashable, so they can be dictionary keys or set members. order=True generates the comparison methods from the field order.

Mutable defaults, once more

tags: list = [] in a dataclass raises an error outright — Python knows the trap. Use tags: list = field(default_factory=list) to give each instance its own list.

default_factory Python
from dataclasses import dataclass, field

@dataclass
class Playlist:
    name: str
    tracks: list = field(default_factory=list)
    plays: int = 0

a = Playlist("Focus")
b = Playlist("Party")

a.tracks.append("Kind of Blue")

print(a)
print(b)          # still empty, as it should be

Type hints

The title: str annotations above are type hints. Python does not enforce them at runtime — but editors, linters and type checkers do, and readers do most of all.

Hints on functions Python
def repeat(text: str, times: int = 2) -> str:
    """Return text repeated, separated by spaces."""
    return " ".join([text] * times)

print(repeat("ha", 3))
print(repeat.__annotations__)

# Nothing stops you passing the wrong type - Python just runs it:
print(repeat(5, 2))

That last line produces a TypeError deep inside join. A type checker such as mypy or ruff would have flagged the call before you ran it.

Hinting collections Python
def total_prices(prices: list[float]) -> float:
    return sum(prices)

def lookup(table: dict[str, int], key: str) -> int | None:
    """Return the value, or None when the key is absent."""
    return table.get(key)

print(total_prices([1.5, 2.5]))
print(lookup({"a": 1}, "a"), lookup({"a": 1}, "z"))

list[float], dict[str, int] and int | None are the modern spellings — no imports needed on Python 3.10 and later.

Properties

Sometimes an attribute should be computed, or validated on assignment. @property makes a method look like an attribute.

Computed and guarded attributes Python
class Rectangle:
    def __init__(self, width: float, height: float):
        self.width = width
        self.height = height

    @property
    def area(self) -> float:
        """Computed on access - no parentheses at the call site."""
        return self.width * self.height

class Temperature:
    def __init__(self, celsius: float):
        self.celsius = celsius

    @property
    def celsius(self) -> float:
        return self._celsius

    @celsius.setter
    def celsius(self, value: float) -> None:
        if value < -273.15:
            raise ValueError("below absolute zero")
        self._celsius = value

    @property
    def fahrenheit(self) -> float:
        return self._celsius * 9 / 5 + 32

r = Rectangle(3, 4)
print(r.area)

t = Temperature(21.5)
print(t.celsius, t.fahrenheit)
t.celsius = -300

Properties let you start with a plain attribute and add validation later without changing a single line of calling code.

Exercise

Model an order

Write a frozen dataclass Item with name: str and price: float, and a dataclass Order with customer: str and items: list[Item] (defaulting to empty). Give Order an add method and a total property.

from dataclasses import dataclass, field

# Your code here
Show one solution
from dataclasses import dataclass, field

@dataclass(frozen=True)
class Item:
    name: str
    price: float

@dataclass
class Order:
    customer: str
    items: list[Item] = field(default_factory=list)

    def add(self, item: Item) -> None:
        self.items.append(item)

    @property
    def total(self) -> float:
        return round(sum(item.price for item in self.items), 2)

order = Order("Ada")
order.add(Item("keyboard", 49.99))
order.add(Item("mouse", 25.50))

print(order)
print(order.total)

Item is frozen because an item in a placed order should not change under you; Order is not, because adding items is the whole point of it.

Day 6 is done

You can now design types that carry their own rules — the step from writing scripts to designing programs.

What you learned

  • @dataclass generates __init__, __repr__ and __eq__ from annotated fields.
  • frozen=True gives immutability; order=True gives comparisons.
  • Use field(default_factory=list) for mutable defaults.
  • Type hints document intent and power editors and type checkers; Python does not enforce them.
  • @property turns a method into a computed or validated attribute.
Last updated on January 10, 2026

Was this article helpful?

Your response is saved on this device.