Dataclasses vs Pydantic
Overview
Standard library `@dataclass` generates boilerplate methods (like `__init__`, `__repr__`) for storing structured data, but it does not strictly enforce types at runtime. Pydantic's `BaseModel` does data parsing, type coercion, and strict validation. Mental model: Dataclasses are lightweight 'storage boxes' trusting the developer to provide correct types. Pydantic models are 'bouncers' that actively inspect, cast, and reject invalid data coming from the outside world. A common edge-case trap is that Pydantic only validates data during initialization. Mutating fields later bypasses checks: class User(BaseModel): age: int u = User(age=20) u.age = 'twenty' # Silently breaks the type contract! To catch this, you must explicitly enable `validate_assignment` in the model configuration.
Choosing the right tool prevents performance overhead when validation isn't needed, and prevents bugs when dealing with untrusted I/O data (like JSON from a web request).
Where used: FastAPI request bodies (Pydantic), Internal application state (`@dataclass`), Environment variable parsing (Pydantic)
Why learn this
- You will know when to pay the performance cost of Pydantic validation.
- You will avoid the false sense of security that standard `@dataclass` type hints give.
Code walkthrough
from pydantic import BaseModel
class Settings(BaseModel):
port: int
s = Settings(port='8080')
print(repr(s.port))
Focus: `s = Settings(port='8080')` causes Pydantic to parse the string `'8080'` and coerce it into the integer `8080`.
Aha moment
from pydantic import BaseModel, ValidationError
class Config(BaseModel):
timeout: int
try:
c = Config(timeout='five')
print(c.timeout)
except ValidationError:
print('Validation Failed!')
Prediction: Will this code crash with a standard Python TypeError or print a message?
Common guess: It will crash with a `TypeError`.
It prints `'Validation Failed!'`. Unlike standard Python types or dataclasses, Pydantic actively intercepts the instantiation, catches the type mismatch, and raises its own rich `ValidationError`.
Common mistakes
- Assuming dataclasses enforce types: If you define `age: int` in a dataclass, passing `age='twenty'` will silently succeed at runtime. Dataclasses only store what they are given unless checked by a static type checker like `mypy`.
- Using Pydantic everywhere: Using Pydantic for purely internal data structures (where you completely control the inputs) adds unnecessary CPU overhead due to its validation checks on every instantiation.
Glossary
- type coercion
- Automatically converting a value from one data type to another, like turning the string `'5'` into the integer `5`, e.g. `int('5')`.
- overhead
- The extra computing power or time required to run a specific feature, like data validation, e.g. Pydantic checking types on `User()`.
Recall questions
- What happens if you pass a string to an `int` field in a standard Python `@dataclass`?
- What is the primary feature Pydantic provides that standard dataclasses do not?
- When is it better to use a standard `@dataclass` over a Pydantic `BaseModel`?
- Does a Pydantic `BaseModel` validate data when you reassign an attribute after the object is created?
Understanding checks
What is printed when this code is executed?
'123'
Standard dataclasses do not perform type validation or coercion at runtime. The string `'123'` is stored exactly as passed, even though the type hint is `int`.
If you receive a JSON payload from an external API, which library should you use to load it into a Python object, and why?
Pydantic.
Because external APIs are untrusted I/O. Pydantic will validate the types and coerce data (e.g., parsing ISO datetime strings into `datetime` objects) or raise a `ValidationError`, whereas a dataclass will blindly accept bad data.
Practice tasks
Upgrade to Validated Models
Given this standard dataclass that blindly accepts bad data, modify it to be a Pydantic `BaseModel` so that it coerces types and validates inputs.
Challenge
Choosing the Right Tool
Create two classes: an internal, unvalidated `InternalState` using `@dataclass` (with field `status: str`), and an external, validated `Payload` using Pydantic's `BaseModel` (with field `amount: float`).