IntermediatePython · Lesson 2 of 9

Classes & Dataclasses

Bundle data and behaviour with classes; remove boilerplate with @dataclass.

A class defines a new type: its data (attributes) and behaviour (methods). __init__ sets up a new object; self refers to the object itself. A leading underscore (_balance) signals "internal — don't touch from outside".

@property exposes a computed or read-only value like an attribute. Special methods such as __repr__ and __eq__ control how objects print and compare.

For classes that are mostly data, @dataclass writes __init__, __repr__ and __eq__ for you. frozen=True makes instances immutable, and field(default_factory=list) gives each object its own list.

classes.pyPython
from dataclasses import dataclass, field


class Account:
    def __init__(self, owner: str) -> None:
        self.owner = owner
        self._balance = 0

    def deposit(self, amount: int) -> None:
        if amount <= 0:
            raise ValueError("deposit must be positive")
        self._balance += amount

    def withdraw(self, amount: int) -> None:
        if amount > self._balance:
            raise ValueError(f"insufficient funds: short by {amount - self._balance}")
        self._balance -= amount

    @property
    def balance(self) -> int:
        return self._balance

    def __repr__(self) -> str:
        return f"Account(owner={self.owner!r}, balance={self._balance})"


@dataclass
class Student:
    name: str
    form: int
    scores: list[int] = field(default_factory=list)

    @property
    def average(self) -> float:
        return sum(self.scores) / len(self.scores) if self.scores else 0.0


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


acc = Account("Amina")
acc.deposit(50_000)
acc.withdraw(20_000)
print(acc, acc.balance)

s = Student("Neema", 4)
s.scores += [71, 84]
print(s, f"average={s.average:.1f}")
print(Subject("BIO", "Biology") == Subject("BIO", "Biology"))   # True
Runs in your browser · Python

Key points

  • Keep internal state behind methods and @property.
  • Use @dataclass for data-holding classes — less code, fewer bugs.
  • Mutable defaults need field(default_factory=...).

Exercise

Build a Library class that manages Book dataclasses (isbn, title, author, available). Add borrow(isbn), give_back(isbn) and available_books(), raising a custom error when a book is already borrowed.

Show solution

Try the exercise yourself first — then compare your approach with this one.

Book is a dataclass because it is mostly data. Library keeps books in a dictionary keyed by ISBN and raises a custom error when a book is already out, so callers can respond to that case specifically.

library.pyPython
from dataclasses import dataclass


class BookAlreadyBorrowedError(Exception):
    pass


@dataclass
class Book:
    isbn: str
    title: str
    author: str
    available: bool = True


class Library:
    def __init__(self) -> None:
        self._books: dict[str, Book] = {}

    def add(self, book: Book) -> None:
        self._books[book.isbn] = book

    def borrow(self, isbn: str) -> Book:
        book = self._books[isbn]               # KeyError for an unknown ISBN
        if not book.available:
            raise BookAlreadyBorrowedError(f"{book.title!r} is already borrowed")
        book.available = False
        return book

    def give_back(self, isbn: str) -> None:
        self._books[isbn].available = True

    def available_books(self) -> list[str]:
        return [b.title for b in self._books.values() if b.available]


library = Library()
library.add(Book("978-1", "Things Fall Apart", "Chinua Achebe"))
library.add(Book("978-2", "Kinjeketile", "Ebrahim Hussein"))

library.borrow("978-1")
print(library.available_books())    # ['Kinjeketile']
try:
    library.borrow("978-1")
except BookAlreadyBorrowedError as err:
    print("Sorry:", err)
library.give_back("978-1")
print(library.available_books())    # ['Things Fall Apart', 'Kinjeketile']
Runs in your browser · Python

Check your understanding

  1. What does @dataclass generate for you?

  2. Why write scores: list[int] = field(default_factory=list) instead of = []?

  3. What does @property let you do?

  4. What does a leading underscore, as in self._balance, signal?

Ask AI