Dataclasses and Pydantic

A dataclass writes __init__, __repr__ and __eq__ for you from typed fields; a Pydantic model goes further and validates the data. Trace both, and watch Pydantic reject a bad order with a ValidationError.

11 min Free

Most classes in agent code exist to hold data: an order, a tool result, a message. Writing __init__ by hand for each one is tedious, and the default printout <OrderItem object at 0x10...> is useless. Python’s @dataclass decorator fixes this: you list the fields with type hints, and it generates __init__, a readable __repr__ and a field-by-field __eq__.

A dataclass trusts whatever you give it. Pass "two" where an int is expected and it stores "two". That is fine for data your own code builds. It is not fine for data that arrives from outside: a model’s JSON output, a file, an API, a user. For that you want Pydantic, a separate package (pip install pydantic) whose BaseModel looks like a dataclass but checks every field when the object is created, converts where safe ("1042" becomes 1042), and raises a ValidationError listing everything wrong.

Press Run for the dataclass scenario. Then switch Try it to “Pydantic model” and watch the last line fail on purpose. To run order.py yourself, create a virtual environment and pip install pydantic first.

Ready
# Two ways to describe a food order: a dataclass, then a Pydantic model.
from dataclasses import dataclass, asdict
from pydantic import BaseModel

@dataclass
class OrderItem:
    name: str
    quantity: int
    price: float = 0.0

item = OrderItem("Masala dosa", 2, 90.0)
print(item)
print(item.quantity * item.price)
print(asdict(item))
print(item == OrderItem("Masala dosa", 2, 90.0))

# A dataclass trusts you: nothing checks the types.
odd = OrderItem("Vada", "two")
print(odd.quantity)

# A Pydantic model validates, and converts where it safely can.
class Order(BaseModel):
    order_id: int
    items: list[str]
    upi_id: str | None = None

order = Order(order_id="1042", items=["idli", "vada"])
print(order.order_id, type(order.order_id).__name__)
print(order.model_dump())

bad = Order(order_id="abc", items="idli")  # raises ValidationError
print("never printed")

Press Play to run the program line by line, or step with ← →.

00/10

Variables

Nothing defined yet.

Output

Nothing printed yet.

What to notice

  1. Fields are annotations. In both styles, name: str in the class body declares a field. The order matters for a dataclass, because the generated __init__ takes the fields positionally, and fields with defaults must come last.
  2. The generated methods save real work. print(item) shows every field; item == other compares values. Use asdict(item) to get a plain dict, for example to return from a tool.
  3. Mutable defaults need field(default_factory=list). tags: list = [] would be shared by every instance, so @dataclass raises ValueError for it. The dataclasses docs show the default_factory fix.
  4. Pydantic validates at construction, once. After Order(...) succeeds, order.order_id is guaranteed to be an int. The rest of your code can stop checking.
  5. One ValidationError reports every problem. Both the bad order_id and the bad items appear, each with the field name and a message. err.errors() gives the same list as dicts, handy for logging or for sending back to a model.
  6. Converting is a choice. By default Pydantic converts "1042" to 1042. Its strict mode refuses any conversion. Pick deliberately for data that must be exact.

Why this matters for AI work

Structured output is built on this. You define a Pydantic model for the shape you want, the framework turns it into a JSON schema for the model, and the model’s reply is validated against it. ADK’s output_schema takes a BaseModel subclass; function tool parameters can be Pydantic models, which ADK converts into instances before your function runs. When the model returns something that does not fit, you get a ValidationError with field names instead of a silent KeyError three functions later.

Check your understanding

1. A @dataclass field is hinted quantity: int and you pass "two". What happens?
2. Order(order_id="abc", items="idli") has two problems. How many exceptions does Pydantic raise?
3. Which data most needs a Pydantic model rather than a dataclass?