Pydantic vs dataclass - When to Use Which?
-
Jason Yang - 30 Nov, 2025
- Updated 04 Jan, 2026
- Views —
Every time I create a data class in Python, I face the same dilemma:
“Should I use Pydantic for this, or is a simple dataclass enough?”
Recently, I ran into this issue while working on a project. I tried to include a Google Cloud Credentials object in a Pydantic model and hit an error. That’s when I finally understood the real difference between these two tools.
In this post, I’ll share practical guidance from real experience on when to use Pydantic and when to use dataclasses.
What Are dataclass and Pydantic?
- Pydantic: Validates all incoming data and automatically converts types.
(Think: Lombok + Bean Validation in Java) - dataclass: Simple data structure for storing values. Generates
__init__,__repr__, and__eq__.
(Think: Python’s built-in Lombok)
Quick Example
# Pydantic - validates data
from pydantic import BaseModel, Field
class User(BaseModel):
name: str
age: int = Field(ge=0, le=150) # Age must be 0–150
user = User(name="Jason", age=30) # ✅ OK
user = User(name="Jason", age=200) # ❌ ValidationError!
# dataclass - just holds data
from dataclasses import dataclass
@dataclass
class User:
name: str
age: int
user = User(name="Jason", age=30) # No validation
Key difference:
Pydantic validates all incoming data, while dataclass is for trusted, internal data.
Quick Selection Guide
| Scenario | Use | Reason |
|---|---|---|
| API request/response | ✅ Pydantic | Input may be untrusted; JSON conversion needed |
| Configuration validation | ✅ Pydantic | Catch invalid values early |
| Internal data passing | ✅ dataclass | No validation needed |
| 3rd-party objects | ✅ dataclass | Pydantic cannot validate unknown external objects |
Real Problem I Faced
❌ Problem Code
from pydantic import BaseModel
from google.auth.credentials import Credentials
class ScopedCredentials(BaseModel):
credentials: Credentials
project_id: str
service_account_email: str | None
Error:
PydanticSchemaGenerationError: Unable to generate pydantic-core schema
for <class 'google.auth.credentials.Credentials'>
Why?
Pydantic tries to validate input data, but Credentials is an external library object that Pydantic doesn’t understand.
✅ Solution Code
from dataclasses import dataclass
from google.auth.credentials import Credentials
@dataclass
class ScopedCredentials:
credentials: Credentials # Just stores without validation
project_id: str
service_account_email: str | None
Why it works:
- dataclass doesn’t validate; it simply creates fields.
- Type hints still enable IDE autocomplete and static type checking.
- No runtime overhead.
Pydantic vs dataclass at a Glance
| Feature | Pydantic | dataclass |
|---|---|---|
| Data validation | ✅ Automatic | ❌ None |
| Type conversion | ✅ "123" → 123 | ❌ None |
| JSON serialization | ✅ .model_dump_json() | ❌ Not provided |
| Performance | Slower (validation overhead) | Faster |
| External objects | ❌ Needs custom config | ✅ Works out-of-the-box |
| Complexity | Medium | Low |
When to Use Pydantic
Example: API Configuration Validation
from pydantic import BaseModel, Field
class VertexAIConfig(BaseModel):
project_id: str = Field(..., min_length=1)
model_name: str = Field(default="gemini-2.0-flash-lite")
timeout: int = Field(default=300, ge=1, le=3600) # 1–3600 seconds
max_tokens: int = Field(default=8192, gt=0)
# ✅ Valid
config = VertexAIConfig(project_id="my-project")
print(config.timeout) # 300
# ❌ Invalid
config = VertexAIConfig(project_id="my-project", timeout=10000)
# ValidationError: timeout must be ≤ 3600
Use Pydantic when:
- Data comes from users or external systems.
- Input validation is needed.
- JSON ↔ Python conversion is required.
Example: FastAPI Request/Response
from fastapi import FastAPI
from pydantic import BaseModel, EmailStr
app = FastAPI()
class LoginRequest(BaseModel):
email: EmailStr
password: str = Field(..., min_length=8)
class LoginResponse(BaseModel):
access_token: str
token_type: str = "bearer"
expires_in: int
@app.post("/login", response_model=LoginResponse)
def login(request: LoginRequest):
...
FastAPI + Pydantic automatically validates requests and generates OpenAPI docs.
When to Use dataclass
Example: Internal Service Layer
from dataclasses import dataclass
from datetime import datetime
from google.auth.credentials import Credentials
@dataclass
class AuthContext:
credentials: Credentials
user_id: str
project_id: str
created_at: datetime
Use dataclass when:
- Data stays internal (service layer, DB entities).
- Data is already validated.
- External objects (like Google Credentials) are included.
- Performance matters (no validation overhead).
Example: Database Query Results
@dataclass
class UserRecord:
id: int
email: str
created_at: datetime
last_login: datetime | None
def get_user_by_id(user_id: int) -> UserRecord | None:
row = db.query("SELECT * FROM users WHERE id = ?", user_id)
if not row:
return None
return UserRecord(**row) # DB data is already trusted
Layer-Based Selection Strategy
┌─────────────────────────────────────┐
│ API Layer (FastAPI) │
│ → Pydantic (input validation) │
│ │
│ class CreateUserRequest(BaseModel) │
│ email: EmailStr │
│ password: str │
└─────────────┬───────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Service Layer │
│ → dataclass (internal logic) │
│ │
│ @dataclass │
│ class UserContext: │
│ user_id: str │
│ credentials: Credentials │
└─────────────┬───────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Data Layer │
│ → dataclass (DB entities) │
│ │
│ @dataclass │
│ class UserRecord: │
│ id: int │
│ email: str │
└─────────────────────────────────────┘
- API Layer: Pydantic ensures all external inputs are safe.
- Service Layer: dataclass passes internal data efficiently.
- Data Layer: dataclass represents DB entities.
Conclusion
- Pydantic and dataclass are complementary, not competitors.
- Pydantic: Guards data at boundaries (API, configs, user input).
- dataclass: Passes trusted data internally (service, DB, logic).
✅ Using the right tool in the right place leads to:
- Simpler code
- Fewer bugs
- Better performance
- Easier maintenance
Don’t overcomplicate with Pydantic everywhere. Validate only where necessary.