Software Engineer's Blog

Pydantic vs dataclass - When to Use Which?

Pydantic vs dataclass - When to Use Which?

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

ScenarioUseReason
API request/response✅ PydanticInput may be untrusted; JSON conversion needed
Configuration validation✅ PydanticCatch invalid values early
Internal data passing✅ dataclassNo validation needed
3rd-party objects✅ dataclassPydantic 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

FeaturePydanticdataclass
Data validation✅ Automatic❌ None
Type conversion"123"123❌ None
JSON serialization.model_dump_json()❌ Not provided
PerformanceSlower (validation overhead)Faster
External objects❌ Needs custom config✅ Works out-of-the-box
ComplexityMediumLow

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.

References