Pydantic fundamentals
What Pydantic actually adds
Everything in this lesson has been building toward one specific gap:
type hints describe what a field should be, but nothing enforces it —
not a plain class, not @dataclass, only a hand-written __post_init__
that has to restate every field’s type as a separate manual check.
Pydantic is a library that closes that gap directly: it reads the same
type hints you’re already writing and turns them into real, automatic
validation, with no __post_init__ needed at all.
The syntax is deliberately close to @dataclass — type-hinted fields,
optional defaults with = — but instead of a decorator on a plain class,
you inherit from BaseModel. That inheritance is what wires in the
validation behavior; everything else about declaring fields looks almost
identical to what you already know.
Validation actually happens now
Pass something that doesn’t match a field’s declared type, and Pydantic
refuses to construct the object at all — no __post_init__, no manual
isinstance checks, none of it written by hand:
Compare this directly to
the manual __post_init__ version from the previous section:
same failure, same field, same moment of failure — construction time, at
the actual mistake — but here it came from temperature: float alone.
The type hint is the validation; there’s nothing else to write.
One thing catching everything — not just the first thing
Recall that the hand-rolled __post_init__ version stopped at whichever
if check happened to run first, reporting only one problem even if
several fields were wrong. Pydantic checks every field and reports every
failure at once, in a single error:
All three problems, in one error, on the first attempt — instead of
fixing one field, re-running, hitting the next if check, and repeating.
This matters in practice for exactly the situation
the previous section
opened with: data coming from somewhere you don’t fully control (an API
response, a config file) is far more likely to have several things wrong
with it at once, not just one.
Type coercion — Pydantic tries to be helpful, within reason
One behavior worth knowing up front, since it can be surprising: Pydantic doesn’t only reject mismatched types — for some conversions it considers safe and unambiguous, it will actually convert the value for you, rather than failing:
"0.9" — a string — was accepted for a float field, and silently
converted to the actual float 0.9. This is deliberate: a numeric-looking
string is a common, unambiguous case (think: a value that arrived as text
from a form or query parameter), so Pydantic coerces it rather than
rejecting it outright. It’s not unlimited, though — "hot" still fails,
as shown earlier, because there’s no reasonable number hiding inside it.
The practical takeaway: Pydantic validates and normalizes reasonable
input — it isn’t purely a strict type-equality check the way isinstance
is.
Getting the data back out: .model_dump()
Just like the hand-written as_dict() method from earlier exercises this
lesson, a Pydantic model can convert itself back into a plain dict — this
comes built in, no method needs to be written:
This closes the loop this lesson opened with — a Pydantic model gives you
the class side’s benefits (fixed, typed fields, autocomplete, methods) and
the dict side’s convenience (a plain dict on demand, exactly like the
@kwargs-collected dicts from earlier in the course), plus the real
validation neither one had on its own.
What does inheriting from BaseModel actually add, compared to a plain class with the same type-hinted fields?
Declare the same three fields as the previous section's ToolCall, but as a BaseModel subclass instead — no __post_init__, no manual checks. The type hints alone should produce the same validation behavior.