- Published on
Today I Learnt: Python Dataclasses
- Authors

- Name
- Mehdi Akiki
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
- Use for Data Storage: Perfect for classes that primarily store data
- Type Hints: Always use type hints for better IDE support and validation
- Immutable When Possible: Use
frozen=Truefor immutable instances - Default Factories: Use
field(default_factory=list)for mutable defaults - Post Init: Use
__post_init__for validation and computed fields - Inheritance: Works well with inheritance for complex hierarchies
- Serialization: Use
asdict()andastuple()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.