@dataclass

The boilerplate a data-holding class always seems to need

AgentConfig from the previous section is a completely ordinary data-holding class — and it already shows a pattern that gets repetitive fast. Every parameter gets typed once in __init__’s signature, then typed again as self.x = x on the next line, for every single field:

class AgentConfig:
    def __init__(self, name: str, model: str, temperature: float = 0.7):
        self.name = name
        self.model = model
        self.temperature = temperature

(not run live — this is the exact class from the previous section, shown again to set up the comparison below)

Three fields, five lines just to store them. And if you want printing or equality to behave sensibly, you’re adding __repr__ and __eq__ by hand too, as covered in the previous lesson:

class AgentConfig:
    def __init__(self, name: str, model: str, temperature: float = 0.7):
        self.name = name
        self.model = model
        self.temperature = temperature

    def __repr__(self):
        return f"AgentConfig(name={self.name!r}, model={self.model!r}, temperature={self.temperature!r})"

    def __eq__(self, other):
        return (
            self.name == other.name
            and self.model == other.model
            and self.temperature == other.temperature
        )

(not run live — illustrating how much boilerplate a fully-featured data-holding class actually needs)

None of this is wrong — it’s exactly what the previous lesson taught — but for a class that’s purely “store some typed fields, nothing more,” writing self.x = x and a matching line in __eq__ for every single field is pure repetition with no real decisions being made.

@dataclass — the same class, generated for you

The dataclasses module’s @dataclass decorator takes type-hinted class attributes and generates __init__, __repr__, and __eq__ automatically, based on nothing more than the fields you declare:

Try it — edit and run

Three lines of field declarations replaced everything from the previous section — no __init__, no __repr__, no __eq__ written by hand, and all three behave exactly like the versions you’d have written yourself: config.name, config.model, and config.temperature all work as plain instance attributes, print(config) shows a readable representation, and == compares by value instead of identity.

This is the same decorator mechanism from the previous lesson@dataclass just happens to be one specifically designed to read a class’s type-hinted attributes and build methods from them, rather than wrapping a function’s behavior the way @log_call did.

= 0.7 on temperature works exactly like a default argument in a regular function — optional to pass, same rules apply: any field with a default must come after every field without one, in declaration order.

Adding your own methods

A @dataclass isn’t limited to auto-generated behavior — it’s still a normal class underneath, so you can add methods exactly as before. The decorator only generates __init__/__repr__/__eq__; anything else you write yourself, the same way:

Try it — edit and run

Overriding a generated method

If you need one of the three generated methods to behave differently than the default, defining it yourself in the class body simply takes priority — @dataclass only fills in what you haven’t already written:

Try it — edit and run

__init__ and __eq__ are still auto-generated as usual here — only __repr__ was overridden, since that’s the only one defined by hand.

What @dataclass doesn't do

It’s worth being precise about what this decorator actually saves you from: boilerplate, not correctness. A @dataclass still has exactly the same gap from the end of the previous section — nothing about @dataclass validates that the values passed in actually match their declared types:

Try it — edit and run

This still runs. @dataclass reads the type hints to know which fields exist and in what order to generate __init__’s parameters — it never checks that what’s actually passed in matches those hints. That’s still the gap ahead: Pydantic, covered later in this lesson, is what actually turns type hints into enforcement.

Check your understanding
1/5

What three methods does @dataclass generate automatically from a class's type-hinted fields?

Exercise · Graded

Declare three type-hinted fields on ToolResult (tool_name: str, output: str, success: bool = True), letting @dataclass generate __init__, then implement as_dict() as a regular method returning the same three values as a plain dict.