← Back to list

Pydantic vs Dataclasses: What I Actually Use in Real Code

1. The Problem.

Zaid Alissa Almaliki · 2025-11-04 08:14 · 0 claps · 5.7 min read paywalled
#python-dataclass #pydantic #python3 #clean-code #data
Open on Medium ↗

Pydantic vs Dataclasses: What I Actually Use in Real Code

The Source of the Image

The Source of the Image

1. The Problem.

2. First Solution With Dataclasses.

3. Second Solution With Pydantic.

  • Type Validation at Creation
  • Type Validation on Assignment
  • Custom Validation Rules
  • Special Types
  • Easy Export to JSON
  • Dictionary Conversion
  • Equality Works

4. When to Use DataClasses Vs Pydantic.

5. Before and After.

6. Conclusion.

Introduction

You’ve written this code hundreds of times. A class that handle some data. Customer information, user profiles, product details. The same pattern over and over:

class Customer:
    def __init__(self, name, id, surname):
        self.name = name
        self.id = id
        self.surname = surname

It works. But it’s boring. And it has deficiencies you ignore until they hit you. Try comparing two clients with similar data. They aren’t equal. Python recognises two distinct objects, not two identical records. Print out a customer for debugging. Instead of the real data, you get an useless memory address. Play with invalid data, like typing a string instead of a number. Your code takes it silently and then breaks somewhere inside in your program, where the error recognition is ineffective.

So you add more code. An __eq__ method for comparisons. A __repr__ method for debugging. Maybe some validation in __init__ to check types. Before you know it, your simple data class contains 30 lines of boilerplate. And you must code this for every structure of data in your application. This isn’t a little problem. Modern applications store information everywhere. Between functions, over API boundaries, into databases, and out as JSON. If you define a new data structure, you have to choose either to write all that boilerplate or ship brittle code that breaks in production.

There is a better way. In fact, there are two more effective approaches, and the best one will depend on what you’re developing. Python’s dataclasses module dramatically decreases programming repetition. One decorator offers identity verification, clear output, and neat syntax. Your class, which was formerly 30 lines long, is now seven lines long. It’s integrated into Python version 3.7 and afterwards, so there’s nothing further to install. But dataclasses do not validate data. They believe everything you pass in. That is fine for internal programs in which you have control over the data flow. It’s not acceptable for processing user input, decoding API replies, or reading configuration files. This is why Pydantic comes in.

Pydantic models resemble dataclasses but include runtime validation. Pass a string that does require an integer? Pydantic detects it promptly and displays an obvious error message. Try assigning faulty data after creation. Pydantic stops you. Do you need to validate an email’s formatting, URL structure, or unique business rules? Pydantic handles it. Additionally, it provides simple JSON serialisation. The distinction between these methods is not academic. Choose dataclasses around internal data formats that allow you to govern the inputs. Pydantic is ideal for validating boundaries, including APIs, user input, and other data sources. Choose standard classes only if you require custom behaviour beyond data storage.

This post demonstrates both techniques using real code. You’ll examine the issues with normal classes, how dataclasses address the majority of them, and why Pydantic’s validation is justified the additional dependency. By the end, you’ll have written less garbage and delivered more resilient software.

Let’s start with the problem.

The Problem

You write a lot of classes that just hold data. Customer records, user profiles, API responses. Each time, you write the same boring code:

class Customer:
    def __init__(self, cus_name, cus_id, cus_last_name):
        self.name = cus_name
        self.id = cus_id
        self.surname = cus_last_name

This works. But it has problems. Try comparing two customers:

ob_1 = Customer("zaid", 1, "alissa")
ob_2 = Customer("zaid", 1, "alissa")
print(ob_1 == ob_2)  # False

They’re not equal, even though they have identical data. Python compares object identity, not values. Try printing one:

print(ob_1)  # <__main__.Customer object at 0x7f8b...>

Useless for debugging. You see a memory address, not the actual data. And there’s no validation. Someone can pass id="oops" and your code breaks later when you try to use it as a number.

First Solution With Dataclasses

Python’s dataclasses fix most of this with one decorator:

from dataclasses import dataclass

@dataclass
class CustomerDataClass:
    name: str
    id: int
    surname: str

Now look what happens:

ob_3 = CustomerDataClass("zaid", 1, "alissa")
ob_4 = CustomerDataClass("zaid", 1, "alissa")

print(ob_3)           # CustomerDataClass(name='zaid', id=1, surname='alissa')
print(ob_3 == ob_4)   # True

What you get:

  • Proper __init__ method
  • Readable __repr__ for debugging
  • Equality comparisons that work
  • Less code to maintain

Example with more fields:

@dataclass
class Person:
    name: str
    age: int
    height: float
    email: str

person = Person('Zaid', 11, 1.11, 'zaid@zaidalissa.me')
print(person)
# Person(name='Zaid', age=11, height=1.11, email='zaid@zaidalissa.me')

Clean. Simple. Done.

Second Solution With Pydantic

Dataclasses are great for internal code. But what if you’re handling external data? User input, API calls, config files. You need validation.

That’s where Pydantic comes in:

from pydantic import BaseModel, EmailStr, field_validator, ValidationError, ConfigDict

class CustomerModel(BaseModel):
    name: str
    id: int
    surname: str
    model_config = ConfigDict(validate_assignment=True, str_strip_whitespace=True)

    @field_validator('name', 'surname')
    @classmethod
    def not_empty(cls, v: str) -> str:
        if not v:
            raise ValueError("must not be empty")
        return v

What this gives you:

  • Type Validation at Creation
try:
    CustomerModel(name="zaid", id="oops", surname="alissa")
except ValidationError as e:
    print(e)
# ValidationError: id must be an integer

Bad data gets caught immediately, not later when your code breaks without warning.

  • Type Validation on Assignment
cm = CustomerModel(name="zaid", id=1, surname="alissa")
try:
    cm.id = "2"
except ValidationError as e:
    print(e)
# ValidationError: id must be an integer

With validate_assignment=True, even changes after creation get validated.

  • Custom Validation Rules
try:
    CustomerModel(name="", id=1, surname="alissa")
except ValidationError as e:
    print(e)
# ValidationError: name must not be empty

You define rules once. Pydantic enforces them everywhere.

  • Special Types
from pydantic import EmailStr

class PersonModel(BaseModel):
    name: str
    age: int
    height: float
    email: EmailStr
    model_config = ConfigDict(validate_assignment=True)
p = PersonModel(name="Zaid", age=11, height=1.11, email="zaid@zaidalissa.me")
print(p)
# PersonModel(name='Zaid', age=11, height=1.11, email='zaid@zaidalissa.me')

EmailStr validates email format automatically. No regex needed. Try a bad email:

try:
    PersonModel(name="Zaid", age=11, height=1.11, email="not-an-email")
except ValidationError as e:
    print(e)
# ValidationError: email is not a valid email address
  • Easy Export to JSON
print(p.model_dump_json(indent=2))
{
  "name": "Zaid",
  "age": 11,
  "height": 1.11,
  "email": "zaid@zaidalissa.me"
}

Perfect for APIs and saving data.

  • Dictionary Conversion
print(p.model_dump())
# {'name': 'Zaid', 'age': 11, 'height': 1.11, 'email': 'zaid@zaidalissa.me'}
  • Equality Works
cm_1 = CustomerModel(name="zaid", id=1, surname="alissa")
cm_2 = CustomerModel(name="zaid", id=1, surname="alissa")

print(cm_1 == cm_2)  # True

When to Use DataClasses Vs Pydantic

Use regular classes when:

  1. You’re prototyping
  2. The class has behavior, not just data
  3. Performance is critical (though the difference is usually tiny)

Use dataclasses when:

  1. You’re holding data internally
  2. You want clean, readable code
  3. You don’t need validation
  4. You’re working with Python 3.7+

Use Pydantic when:

  1. You’re handling external data (APIs, user input, config files)
  2. You need validation
  3. You’re building web APIs (FastAPI uses Pydantic)
  4. You want automatic documentation
  5. You need JSON serialization

Before and After

Before (regular class):

class Person:
    def __init__(self, name, age, height, email):
        self.name = name
        self.age = age
        self.height = height
        self.email = email

    def __repr__(self):
        return f'Person(name={self.name}, age={self.age}, height={self.height}, email={self.email})'

person = Person('Zaid', 11, 1.11, 'zaid@zaidalissa.me')

12 lines of boilerplate.

After (dataclass):

@dataclass
class Person:
    name: str
    age: int
    height: float
    email: str

person = Person('Zaid', 11, 1.11, 'zaid@zaidalissa.me')

7 lines. Same result.

After (Pydantic with validation):

class PersonModel(BaseModel):
    name: str
    age: int
    height: float
    email: EmailStr
    model_config = ConfigDict(validate_assignment=True)

p = PersonModel(name="Zaid", age=11, height=1.11, email="zaid@zaidalissa.me")

7 lines. Plus type checking, validation, and JSON export.

Conclusion

Stop fucking around, cunt. I mean writing simple data classes. Use dataclasses for internal structures. Use Pydantic for external boundaries. The choice is yours. If you control the input and the data, dataclasses give you clean code with minimal effort. One decorator eliminates the boring parts — equality checks, readable output, initialization, and you move on before you moved on. If you’re handling external data, user input, API calls, or config files, Pydantic’s validation saves you from runtime errors. Bad data gets caught at the boundary with clear error messages. You define validation rules once and Pydantic enforces them everywhere. The extra dependency is worth it.

Both approaches share a philosophy: data classes should be simple to write and easy to use. Python’s traditional approach in which you’re writing __init__, __repr__, and __eq__ by hand it wastes time and create bugs. Dataclasses and Pydantic eliminate wasting your time and invest in doing something better. Your code gets shorter. Your bugs get caught in a blink of an eye. Your debugging gets easier and funnier. And you spend less time writing boilerplate code and more time solving real problems, problems that can reshape human destiny. Pick the tool that fits your requirments. Use dataclasses inside your application. Use Pydantic at the frontier of interaction with external data. And stop writing classes the old way.


메타데이터
post_id
b809e4bf33f6
slug
pydantic-vs-dataclasses-what-i-actually-use-in-real-code-b809e4bf33f6
url
https://medium.com/@dataakkadian/pydantic-vs-dataclasses-what-i-actually-use-in-real-code-b809e4bf33f6
canonical_url
https://medium.com/@dataakkadian/pydantic-vs-dataclasses-what-i-actually-use-in-real-code-b809e4bf33f6
author_url
https://medium.com/@dataakkadian
status
ok
fetched_at
2026-07-30 14:39:31