Mehdi Akiki
Published on

Today I Learnt: Python Dataclasses

Authors
  • Mehdi Akiki avatar
    Name
    Mehdi Akiki
    Twitter

Reference

Today I Learnt: Python Dataclasses

Python's dataclasses module, introduced in Python 3.7, is a powerful decorator that automatically generates boilerplate code for classes that primarily store data. It eliminates the need to write repetitive __init__, __repr__, and other methods.

Basic Usage

from dataclasses import dataclass

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

# Automatically generates:
# - __init__(self, name, age, email)
# - __repr__(self)
# - __eq__(self, other)
# - __hash__(self) if all fields are hashable

person = Person("John Doe", 30, "[email protected]")
print(person)  # Person(name='John Doe', age=30, email='[email protected]')

Field Options

Default Values

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

# Usage
user1 = User("johndoe", "[email protected]")  # Uses defaults
user2 = User("janedoe", "[email protected]", False, "admin")  # Override defaults

Field Customization

from dataclasses import dataclass, field

@dataclass
class Product:
    name: str
    price: float
    tags: list = field(default_factory=list)  # Mutable default
    id: int = field(init=False)  # Not in __init__

    def __post_init__(self):
        self.id = hash(self.name + str(self.price))

# Usage
product = Product("Laptop", 999.99, ["electronics", "computers"])
print(product.id)  # Auto-generated ID

Advanced Features

Inheritance

@dataclass
class Animal:
    name: str
    species: str

@dataclass
class Pet(Animal):
    owner: str
    vaccinated: bool = True

# Usage
pet = Pet("Fluffy", "Cat", "John Doe", True)
print(pet)  # Pet(name='Fluffy', species='Cat', owner='John Doe', vaccinated=True)

Custom Methods

@dataclass
class Rectangle:
    width: float
    height: float

    @property
    def area(self):
        return self.width * self.height

    def scale(self, factor: float):
        return Rectangle(self.width * factor, self.height * factor)

# Usage
rect = Rectangle(10, 5)
print(f"Area: {rect.area}")  # Area: 50.0
scaled = rect.scale(2)
print(scaled)  # Rectangle(width=20.0, height=10.0)

Field Validation

from dataclasses import dataclass, field

@dataclass
class EmailUser:
    name: str
    email: str

    def __post_init__(self):
        if '@' not in self.email:
            raise ValueError("Invalid email address")
        if not self.name.strip():
            raise ValueError("Name cannot be empty")

# Usage
try:
    user = EmailUser("John", "invalid-email")
except ValueError as e:
    print(e)  # Invalid email address

Comparison with Other Approaches

vs Regular Classes

# Regular class (verbose)
class PersonRegular:
    def __init__(self, name, age, email):
        self.name = name
        self.age = age
        self.email = email

    def __repr__(self):
        return f"PersonRegular(name={self.name!r}, age={self.age}, email={self.email!r})"

    def __eq__(self, other):
        if not isinstance(other, PersonRegular):
            return False
        return (self.name, self.age, self.email) == (other.name, other.age, other.email)

# Dataclass (clean)
@dataclass
class PersonData:
    name: str
    age: int
    email: str

vs Named Tuples

from collections import namedtuple

# Named tuple (immutable, no defaults)
PersonNT = namedtuple('PersonNT', ['name', 'age', 'email'])
person_nt = PersonNT("John", 30, "[email protected]")

# Dataclass (mutable, defaults, methods)
@dataclass
class PersonDC:
    name: str
    age: int
    email: str = "[email protected]"

    def greet(self):
        return f"Hello, I'm {self.name}"

vs Dictionaries

# Dictionary (no type safety, verbose access)
person_dict = {"name": "John", "age": 30, "email": "[email protected]"}
print(person_dict["name"])  # Access via keys

# Dataclass (type safe, clean access)
@dataclass
class PersonDC:
    name: str
    age: int
    email: str

person_dc = PersonDC("John", 30, "[email protected]")
print(person_dc.name)  # Direct attribute access

Real-World Use Cases

API Data Models

from dataclasses import dataclass
from typing import Optional, List
from datetime import datetime

@dataclass
class User:
    id: int
    username: str
    email: str
    created_at: datetime
    is_active: bool = True
    profile: Optional['UserProfile'] = None

@dataclass
class UserProfile:
    bio: str
    avatar_url: Optional[str] = None
    website: Optional[str] = None
    skills: List[str] = field(default_factory=list)

# Usage
user = User(
    id=1,
    username="johndoe",
    email="[email protected]",
    created_at=datetime.now(),
    profile=UserProfile(
        bio="Python developer",
        skills=["Python", "Django", "React"]
    )
)

Configuration Classes

@dataclass
class DatabaseConfig:
    host: str = "localhost"
    port: int = 5432
    database: str = "mydb"
    user: str = "admin"
    password: str = field(repr=False)  # Don't show in repr
    ssl_mode: str = "require"

@dataclass
class AppConfig:
    debug: bool = False
    database: DatabaseConfig = field(default_factory=DatabaseConfig)
    cache_ttl: int = 300
    max_connections: int = 100

# Usage
config = AppConfig(
    debug=True,
    database=DatabaseConfig(
        host="prod-db.example.com",
        password="secret123"
    )
)

Event/Data Transfer Objects

from dataclasses import dataclass, asdict, astuple
from typing import Any

@dataclass
class Event:
    type: str
    timestamp: datetime
    user_id: int
    data: dict = field(default_factory=dict)

    def to_dict(self):
        return asdict(self)

    def to_tuple(self):
        return astuple(self)

# Usage
event = Event(
    type="user_login",
    timestamp=datetime.now(),
    user_id=123,
    data={"ip": "192.168.1.1", "user_agent": "Chrome"}
)

# Convert to different formats
event_dict = event.to_dict()
event_tuple = event.to_tuple()

Performance Considerations

import time
from dataclasses import dataclass

@dataclass
class DataClassPerson:
    name: str
    age: int
    email: str

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

# Performance comparison
def benchmark_class_creation(cls, iterations=100000):
    start = time.time()
    for _ in range(iterations):
        instance = cls("John", 30, "[email protected]")
    end = time.time()
    return end - start

# Dataclasses are slightly slower due to metadata overhead
# but the difference is usually negligible for most applications
dataclass_time = benchmark_class_creation(DataClassPerson)
regular_time = benchmark_class_creation(RegularClassPerson)

print(f"Dataclass: {dataclass_time:.4f}s")
print(f"Regular: {regular_time:.4f}s")

Best Practices

  1. Use for Data Storage: Perfect for classes that primarily store data
  2. Type Hints: Always use type hints for better IDE support and validation
  3. Immutable When Possible: Use frozen=True for immutable instances
  4. Default Factories: Use field(default_factory=list) for mutable defaults
  5. Post Init: Use __post_init__ for validation and computed fields
  6. Inheritance: Works well with inheritance for complex hierarchies
  7. Serialization: Use asdict() and astuple() for easy conversion

Common Patterns

Frozen Dataclasses

@dataclass(frozen=True)
class ImmutablePoint:
    x: int
    y: int

point = ImmutablePoint(1, 2)
# point.x = 3  # This will raise an error

Ordered Dataclasses

from dataclasses import dataclass, field

@dataclass(order=True)
class Version:
    major: int = field(compare=False)  # Don't compare major version
    minor: int
    patch: int

v1 = Version(1, 2, 3)
v2 = Version(1, 3, 1)

print(v1 < v2)  # True (compares minor, then patch)

Dataclasses are a fantastic addition to Python that reduce boilerplate code while maintaining readability and type safety. They're especially useful for data transfer objects, configuration classes, and any scenario where you need simple, clean data structures.