Topic 02

Dataclasses: The Short Form

Hands-on

Most classes a script needs are records: a few named fields, a readable printout, and a sensible way to compare two of them. Writing __init__, __repr__ and a comparison by hand for every such class is tedious and easy to get slightly wrong. Python's dataclasses module writes them for you from a simple list of fields.

Think of a pre-printed form instead of a blank sheet of paper. The fields are already laid out and labelled; you only fill them in. A dataclass is that form.

By Hand

Here is a book record written the way the last topic taught. It works, but look at how much of it is repetition: every field appears three times in __init__ alone. And one thing silently does not work: two books with exactly the same details compare as different, because without a comparison method Python only checks whether they are the very same object.

book_by_hand.py
class Book:
    def __init__(self, title, author, pages):
        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})"


a = Book("Dune", "Frank Herbert", 412)
b = Book("Dune", "Frank Herbert", 412)
print(a)
print(a == b)
Output
Book(title='Dune', author='Frank Herbert', pages=412)
False

@dataclass

The same record as a dataclass. The line @dataclass above the class is a decorator: it takes the class written below it and adds methods to it. Inside, each field is listed once with a colon and its type. From that list, the decorator writes __init__, __repr__, and an equality check that compares the fields.

book.py
from dataclasses import dataclass


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


a = Book("Dune", "Frank Herbert", 412)
b = Book("Dune", "Frank Herbert", 412)
print(a)
print(a == b)
Output
Book(title='Dune', author='Frank Herbert', pages=412)
True

Five lines instead of seven, and equality now works: two books with the same fields are equal.

Type Annotations

The words after the colons, str and int, are type annotations. They document what each field is meant to hold, and dataclasses need a field to have one to recognise it. They are not checked when the program runs: Book("Dune", "Frank Herbert", "many") would be accepted without complaint. Separate tools can check annotations before a program runs; this book only asks you to read them.

Defaults and Methods

A field can have a default, written with an equals sign, and it is used when the caller leaves that field out. Fields with defaults must come after the ones without. And a dataclass is still an ordinary class, so you can add methods to it exactly as in the last topic.

book_more.py
from dataclasses import dataclass


@dataclass
class Book:
    title: str
    author: str
    pages: int
    format: str = "paperback"

    def is_long(self):
        """Return True for books over 400 pages."""
        return self.pages > 400


dune = Book("Dune", "Frank Herbert", 412)
print(dune)
print(dune.is_long())
Output
Book(title='Dune', author='Frank Herbert', pages=412, format='paperback')
True

Frozen Dataclasses

@dataclass(frozen=True) makes records that cannot be changed once created, like the tuples of Chapter 6. Trying to change a field raises a FrozenInstanceError, saying the field cannot be assigned. Frozen records are useful for values that should never drift, such as a transaction read from a bank statement.

frozen.py
from dataclasses import dataclass


@dataclass(frozen=True)
class Book:
    title: str
    pages: int


dune = Book("Dune", 412)
dune.pages = 500
Output
Traceback (most recent call last):
  File "/Users/you/python-course/frozen.py", line 11, in <module>
    dune.pages = 500
    ^^^^^^^^^^
  File "<string>", line 16, in __setattr__
dataclasses.FrozenInstanceError: cannot assign to field 'pages'

The middle frame names a file called string: that is the code the decorator wrote for you, which has no file of its own. As usual, the last line is the one to read.

The same record, two ways
By hand
Seven lines: __init__ and __repr__ written out, and == compares identity.
With @dataclass
Five lines: the fields listed once, the same behaviour, and == compares fields.
Common Confusions
  • "Type annotations are checked when the program runs." They are not. They document intent, and separate tools can check them, but Python itself accepts any value.
  • "A dataclass is a different kind of thing from a class." It is an ordinary class with some methods written for you. You can still add your own.
  • "Two equal records are the same object." With a dataclass they compare equal with ==, but they are still two separate objects.
Why It Matters
  • Records are the most common reason to write a class, and a dataclass turns each one into a few readable lines.
  • Dataclasses are used widely in modern Python, including in Backend Deep Dive, so you will read them often.

Knowledge Check

What does @dataclass write for you?

  • __init__, __repr__ and an equality check
  • A database table for storing the records
  • Checks that every field has the right type
  • A copy of the class for each object that is made

Are type annotations like pages: int checked when the program runs?

  • Yes, and a wrong type raises a TypeError at once
  • No, they document the intended type only
  • Only for numbers, never for text
  • Only when the dataclass is frozen

Why did the hand-written Book compare two identical books as not equal?

  • Because their page counts were stored as text rather than numbers
  • Because __repr__ printed the two of them differently on screen
  • Without its own equality method, == checks for the same object
  • Because objects of a class can never be compared with == at all

What does frozen=True do to a dataclass?

  • It makes the class faster to create objects from
  • It stops new objects being made from the class
  • It stores the objects in a file, so they persist between runs
  • Its objects cannot be changed after they are made

You got correct