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.

Try it — edit and run

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:

Try it — edit and run

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:

Try it — edit and run

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:

Try it — edit and run

"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:

Try it — edit and run

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.

Check your understanding
1/5

What does inheriting from BaseModel actually add, compared to a plain class with the same type-hinted fields?

Exercise · Graded

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.