@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:
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:
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:
__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:
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.
What three methods does @dataclass generate automatically from a class's type-hinted fields?
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.