Python Dataclasses vs Pydantic Models for Internal Domain Objects
Quick answer
Use Pydantic models at the edges of your system, where untrusted data enters and needs validation, and use plain dataclasses for internal domain objects whose...
Use Pydantic models at the edges of your system, where untrusted data enters and needs validation, and use plain dataclasses for internal domain objects whose data is already trusted. Dataclasses are lightweight and have no validation overhead. Pydantic gives you parsing, coercion, and rich error reporting, which is valuable at boundaries but often unnecessary deep inside your code.
What Is the Core Difference Between the Two?
A dataclass is a standard library feature that generates an initializer, representation, and comparison methods from type annotations. The annotations are hints only. Python does not enforce them at runtime, so a dataclass field annotated as int will happily hold a string.
Pydantic models treat annotations as a contract. When you create a model, the library validates the input, coerces compatible values where its configuration allows it, and raises a detailed error if the data does not fit. That behavior is exactly what you want when data arrives from an HTTP request, a message queue, or a configuration file.
from dataclasses import dataclass
from pydantic import BaseModel
@dataclass(frozen=True)
class Money:
amount_cents: int
currency: str
class CreateOrderRequest(BaseModel):
customer_id: int
amount_cents: int
currency: str
Static type checkers understand both. A dataclass with annotations gives your editor and tools like mypy the information they need, even though nothing is checked when the program runs. Pydantic adds the runtime check on top of that same static information.
Where Should You Validate Data?
Validate once, at the boundary. Parse incoming JSON into a Pydantic model, check business rules, and then convert it into a domain object. Inside the domain, functions can assume the data is already valid, which keeps the code simpler and avoids repeating checks.
If every internal object is a validating model, the same checks run repeatedly as data moves between layers. That costs CPU time and creates noise, and it tempts developers to rely on coercion instead of fixing wrong types at the source. Keeping the domain layer plain makes its logic easier to read and test.
- Boundary objects: request bodies, responses, settings, and events use Pydantic.
- Domain objects: entities and value objects use dataclasses, often frozen for immutability.
- Persistence objects: follow your ORM's conventions and map to the domain at the repository.
How Do Performance and Maintenance Compare?
Modern Pydantic versions implement validation in a compiled core and are fast, but dataclasses still avoid the work entirely. For a few objects per request the difference is negligible. For code that creates many thousands of small objects in a tight loop, the overhead of validation can matter, so measure with your own workload and version rather than assuming.
Maintenance matters more than microbenchmarks. Pydantic brings a dependency and a set of behaviors, such as coercion rules and configuration options, that change between major versions. Dataclasses come with the language and rarely surprise anyone. If a module has no need for parsing, a dataclass keeps it free of that coupling.
Can You Combine Them?
Yes. Pydantic supports its own dataclass decorator, and models can be converted to and from dataclasses and dictionaries. A common pattern is a small mapping function in the application layer that turns a validated request model into a domain dataclass. That function is a natural place for business rules that go beyond field validation.
Avoid making the domain depend on the web layer's models. Importing request classes into core business logic couples the domain to your API format, and changing an endpoint then forces changes deep in the code.
If you are unsure where a class belongs, ask where its data comes from. If it comes from outside your process, validate it. If it was built by your own trusted code, a dataclass is usually enough, and any mistakes will be caught by tests and static analysis rather than runtime parsing.
Frequently Asked Questions
Do dataclasses validate types at runtime?
No. Annotations are not enforced. You can add checks in __post_init__, but then you are rebuilding part of what Pydantic already provides.
Is Pydantic slower than dataclasses?
Validation adds cost, though recent versions are fast. For most services the difference is small, so choose based on where validation belongs.
Should my ORM models double as domain objects?
For small applications that can be fine. As complexity grows, separating domain objects from persistence models reduces coupling and makes testing easier.
Should domain dataclasses be frozen?
Often yes, for value objects. Immutability prevents accidental changes and makes objects safe to share and use as dictionary keys.
Summary
Validate untrusted data with Pydantic at the boundary, then work with lightweight dataclasses inside the domain. This keeps validation in one place, reduces overhead, and limits coupling. Check the documentation for your Pydantic version, since behavior changed between major releases.