Skip to the content.

Author: Amin Boulouma, Software Engineer Github source code: https://github.com/aminblm/ai_systems_design_from_scratch Engineering Blog: https://aminblm.github.io/ai_systems_design_from_scratch/blog/

5 Reasons You Should Abandon Standard Classes for DataClasses

In Python, we often write classes that primarily serve to store data. Traditionally, this meant boilerplate-heavy code: writing __init__, __repr__, and __eq__ methods over and over. Since Python 3.7, DataClasses have arrived to automate this drudgery, allowing us to define structured data containers with minimal effort.


The Core Concept

A DataClass is a decorator-based construct that automatically generates dunder methods (like __init__, __repr__, and __eq__) based on the type-annotated fields you define. It shifts the focus from “how to store this data” to “what data is being stored.”

Glossary for Beginners


Why We Choose DataClasses over Standard Classes

We choose DataClasses because they are explicit and compact. Standard classes hide the “data shape” inside the __init__ method, making it harder to inspect. DataClasses elevate the definition of the fields to the top level of the class body.

Why X over Y? We choose dataclasses over NamedTuple when we need mutability or the ability to define methods within the class. We choose them over standard classes because they reduce human error in implementing equality checks and string representations.


Implementation: The DataClass Pattern

Simple Example: Basic Usage

from dataclasses import dataclass

@dataclass
class User:
    username: str
    email: str
    active: bool = True

# Automatically provides __init__, __repr__, and __eq__
user = User(username="admin", email="admin@example.com")
print(user) # Output: User(username='admin', email='admin@example.com', active=True)

Complex Example: Production-Grade Configuration

In complex systems, we often need validation after initialization. The __post_init__ method is the standard way to handle this in DataClasses.

from dataclasses import dataclass, field

@dataclass(frozen=True) # Makes instances immutable
class DatabaseConfig:
    host: str
    port: int
    timeout: float = 30.0
    tags: list[str] = field(default_factory=list)

    def __post_init__(self):
        # Enforce validation post-instantiation
        if self.port < 1024:
            raise ValueError("Privileged ports not allowed")

# Usage
try:
    config = DatabaseConfig(host="localhost", port=80)
except ValueError as e:
    print(f"Configuration error: {e}")

Quick Reference: DataClass Strategies

Strategy When to use Pros Cons
Basic DataClass Simple data storage Reduces boilerplate None
frozen=True Immutable objects Thread-safe, hashable No modification
__post_init__ Data validation Clean logic separation Slightly slower

Developer Checklist

TL;DR Summary

Stop writing __init__ for simple objects. Use DataClasses to reduce boilerplate, improve readability, and gain automatic support for comparisons and string representation. Always use default_factory for mutable defaults like lists to avoid the common “shared state” bug.