@dataclass and Pydantic's BaseModel look almost identical on the page โ a class, some typed fields, no boilerplate constructor. They are not interchangeable. The difference comes down to one question: do you trust your input?
What a Dataclass Actually Does
@dataclass is a code generator. It writes __init__, __repr__, and __eq__ for you based on the annotations. That is all. It does not check types at runtime.
from dataclasses import dataclass @dataclass class Point: x: int y: int Point(x="hello", y=None) # totally fine โ no validation
The annotations are hints for humans and mypy, nothing more.
What Pydantic Adds
Pydantic parses and validates at construction time. Give it the wrong type and it either coerces or raises.
from pydantic import BaseModel class Point(BaseModel): x: int y: int Point(x="hello", y=None) # raises ValidationError
It also gives you JSON serialization, schema generation, and field constraints for free.
The Mental Model
- โธTrusted, internal data โ dataclass. You constructed it in your own code, the shapes are guaranteed, and you want a lightweight container with zero dependencies. Dataclasses are part of the standard library and faster to instantiate.
- โธUntrusted, external data โ Pydantic. It came from an HTTP request, a config file, a message queue, or a third-party API. You need it validated and parsed before you trust it. This is exactly why FastAPI is built on Pydantic.
A Simple Rule
Draw the line at your program's boundary. Data crossing in from the outside world gets a Pydantic model so it is validated once, at the edge. Data flowing within your trusted core can be plain dataclasses โ cheaper and dependency-free.
Summary
Dataclasses remove boilerplate; Pydantic removes boilerplate and validates. Use dataclasses for internal, already-trusted structures and Pydantic wherever data enters your system from outside. Choosing by that boundary keeps validation where it belongs and overhead where it does not hurt.