Dataclasses Python : guide complet du décorateur @dataclass
Updated on
Écrire des classes Python implique souvent du code répétitif (boilerplate). Vous définissez __init__ pour initialiser les attributs, __repr__ pour un affichage lisible, __eq__ pour les comparaisons, et parfois __hash__ pour la hashabilité. Cette implémentation manuelle devient fastidieuse pour les classes destinées à contenir des données, notamment lors de la gestion d’objets de configuration, de réponses d’API ou d’enregistrements de base de données.
Python 3.7 a introduit les dataclasses via la PEP 557, automatisant ce boilerplate tout en conservant la flexibilité des classes classiques. Le décorateur @dataclass génère automatiquement des méthodes spéciales à partir des annotations de types, réduisant du code de dizaines de lignes à seulement quelques-unes. Ce guide montre comment exploiter les dataclasses pour obtenir un code Python plus propre et plus maintenable.
Pourquoi les dataclasses existent : résoudre le problème du boilerplate
Les classes Python traditionnelles exigent de définir explicitement des méthodes pour les opérations courantes. Considérez cette classe standard pour stocker des données utilisateur :
class User:
def __init__(self, name, email, age):
self.name = name
self.email = email
self.age = age
def __repr__(self):
return f"User(name={self.name!r}, email={self.email!r}, age={self.age!r})"
def __eq__(self, other):
if not isinstance(other, User):
return NotImplemented
return (self.name, self.email, self.age) == (other.name, other.email, other.age)Avec les dataclasses, cela se réduit à :
from dataclasses import dataclass
@dataclass
class User:
name: str
email: str
age: intLe décorateur génère automatiquement __init__, __repr__ et __eq__ à partir des annotations de types. Cela élimine plus de 15 lignes de boilerplate tout en conservant des fonctionnalités identiques.
Syntaxe de base de @dataclass
La dataclass la plus simple ne nécessite que des annotations de types pour les champs :
from dataclasses import dataclass
@dataclass
class Product:
name: str
price: float
quantity: int
product = Product("Laptop", 999.99, 5)
print(product) # Product(name='Laptop', price=999.99, quantity=5)
product2 = Product("Laptop", 999.99, 5)
print(product == product2) # TrueLe décorateur accepte des paramètres pour personnaliser le comportement :
@dataclass(
init=True, # Générer __init__ (par défaut : True)
repr=True, # Générer __repr__ (par défaut : True)
eq=True, # Générer __eq__ (par défaut : True)
order=False, # Générer les méthodes de comparaison (par défaut : False)
frozen=False, # Rendre les instances immuables (par défaut : False)
unsafe_hash=False # Générer __hash__ (par défaut : False)
)
class Config:
host: str
port: intTypes de champs et valeurs par défaut
Les dataclasses prennent en charge des valeurs par défaut pour les champs. Les champs sans valeur par défaut doivent apparaître avant les champs avec valeur par défaut :
from dataclasses import dataclass
@dataclass
class Server:
host: str
port: int = 8080
protocol: str = "http"
server1 = Server("localhost")
print(server1) # Server(host='localhost', port=8080, protocol='http')
server2 = Server("api.example.com", 443, "https")
print(server2) # Server(host='api.example.com', port=443, protocol='https')Pour les valeurs par défaut mutables (listes ou dictionnaires), utilisez default_factory afin d’éviter les références partagées :
from dataclasses import dataclass, field
# WRONG - all instances share the same list
@dataclass
class WrongConfig:
tags: list = [] # Raises error in Python 3.10+
# CORRECT - each instance gets a new list
@dataclass
class CorrectConfig:
tags: list = field(default_factory=list)
metadata: dict = field(default_factory=dict)
config1 = CorrectConfig()
config2 = CorrectConfig()
config1.tags.append("production")
print(config1.tags) # ['production']
print(config2.tags) # [] - separate listLa fonction field() : configuration avancée des champs
La fonction field() fournit un contrôle fin sur les champs individuels :
from dataclasses import dataclass, field
from typing import List
@dataclass
class Employee:
name: str
employee_id: int
salary: float = field(repr=False) # Hide salary in repr
skills: List[str] = field(default_factory=list)
_internal_id: str = field(init=False, repr=False) # Not in __init__
performance_score: float = field(default=0.0, compare=False) # Exclude from comparison
def __post_init__(self):
self._internal_id = f"EMP_{self.employee_id:06d}"
emp = Employee("Alice", 12345, 85000.0, ["Python", "SQL"])
print(emp) # Employee(name='Alice', employee_id=12345, skills=['Python', 'SQL'], performance_score=0.0)
print(emp._internal_id) # EMP_012345Principaux paramètres de field() :
| Paramètre | Type | Description |
|---|---|---|
default | Any | Valeur par défaut du champ |
default_factory | Callable | Fonction sans argument renvoyant la valeur par défaut |
init | bool | Inclure le champ dans __init__ (par défaut : True) |
repr | bool | Inclure le champ dans __repr__ (par défaut : True) |
compare | bool | Inclure le champ dans les méthodes de comparaison (par défaut : True) |
hash | bool | Inclure le champ dans __hash__ (par défaut : None) |
metadata | dict | Métadonnées arbitraires (non utilisées par le module dataclasses) |
kw_only | bool | Rendre le champ keyword-only (Python 3.10+) |
Le paramètre metadata stocke des informations arbitraires accessibles via fields() :
from dataclasses import dataclass, field, fields
@dataclass
class APIRequest:
endpoint: str = field(metadata={"description": "API endpoint path"})
method: str = field(default="GET", metadata={"choices": ["GET", "POST", "PUT", "DELETE"]})
for f in fields(APIRequest):
print(f"{f.name}: {f.metadata}")
# endpoint: {'description': 'API endpoint path'}
# method: {'choices': ['GET', 'POST', 'PUT', 'DELETE']}Annotations de types avec les dataclasses
Les dataclasses s’appuient sur les annotations de types, mais ne les appliquent pas à l’exécution. Utilisez le module typing pour les types complexes :
from dataclasses import dataclass
from typing import List, Dict, Optional, Union, Tuple
from datetime import datetime
@dataclass
class DataAnalysisJob:
job_id: str
dataset_path: str
columns: List[str]
filters: Dict[str, Union[str, int, float]]
output_format: str = "csv"
created_at: datetime = field(default_factory=datetime.now)
completed_at: Optional[datetime] = None
error_message: Optional[str] = None
results: Optional[Dict[str, Tuple[float, float]]] = None
job = DataAnalysisJob(
job_id="job_001",
dataset_path="/data/sales.csv",
columns=["date", "revenue", "region"],
filters={"year": 2026, "region": "US"}
)Pour une vérification de types à l’exécution, intégrez des bibliothèques comme pydantic ou effectuez une validation dans __post_init__.
frozen=True : créer des dataclasses immuables
Définissez frozen=True pour rendre les instances immuables après création, de manière similaire aux named tuples :
from dataclasses import dataclass
@dataclass(frozen=True)
class Point:
x: float
y: float
def distance_from_origin(self):
return (self.x**2 + self.y**2) ** 0.5
point = Point(3.0, 4.0)
print(point.distance_from_origin()) # 5.0
# Attempting to modify raises FrozenInstanceError
try:
point.x = 5.0
except AttributeError as e:
print(f"Error: {e}") # Error: cannot assign to field 'x'Les dataclasses frozen sont hashables par défaut si tous les champs sont hashables, ce qui permet de les utiliser dans des sets et comme clés de dictionnaire :
@dataclass(frozen=True)
class Coordinate:
latitude: float
longitude: float
locations = {
Coordinate(40.7128, -74.0060): "New York",
Coordinate(51.5074, -0.1278): "London"
}
print(locations[Coordinate(40.7128, -74.0060)]) # New YorkMéthode post_init : validation et champs calculés
La méthode __post_init__ s’exécute après __init__, permettant la validation et l’initialisation de champs calculés :
from dataclasses import dataclass, field
from datetime import datetime
@dataclass
class BankAccount:
account_number: str
balance: float
created_at: datetime = field(default_factory=datetime.now)
account_type: str = field(init=False)
def __post_init__(self):
if self.balance < 0:
raise ValueError("Initial balance cannot be negative")
# Compute account_type based on balance
if self.balance >= 100000:
self.account_type = "Premium"
elif self.balance >= 10000:
self.account_type = "Gold"
else:
self.account_type = "Standard"
account = BankAccount("ACC123456", 50000.0)
print(account.account_type) # GoldPour des champs init=False qui dépendent d’autres champs, utilisez __post_init__ :
from dataclasses import dataclass, field
@dataclass
class Rectangle:
width: float
height: float
area: float = field(init=False)
perimeter: float = field(init=False)
def __post_init__(self):
self.area = self.width * self.height
self.perimeter = 2 * (self.width + self.height)
rect = Rectangle(5.0, 3.0)
print(f"Area: {rect.area}, Perimeter: {rect.perimeter}") # Area: 15.0, Perimeter: 16.0Héritage avec les dataclasses
Les dataclasses prennent en charge l’héritage avec fusion automatique des champs :
from dataclasses import dataclass
@dataclass
class Animal:
name: str
age: int
@dataclass
class Dog(Animal):
breed: str
is_good_boy: bool = True
dog = Dog("Buddy", 5, "Golden Retriever")
print(dog) # Dog(name='Buddy', age=5, breed='Golden Retriever', is_good_boy=True)Les sous-classes héritent des champs du parent et peuvent en ajouter de nouveaux. Les champs sans valeur par défaut ne peuvent pas suivre des champs avec valeur par défaut à travers l’héritage :
from dataclasses import dataclass
@dataclass
class BaseConfig:
environment: str = "production"
# ERROR: Non-default field 'api_key' cannot follow default field 'environment'
# @dataclass
# class APIConfig(BaseConfig):
# api_key: str
# CORRECT: Use default or rearrange fields
@dataclass
class APIConfig(BaseConfig):
api_key: str = "" # Provide default
timeout: int = 30Python 3.10+ a introduit kw_only pour résoudre ce point :
from dataclasses import dataclass
@dataclass
class BaseConfig:
environment: str = "production"
@dataclass(kw_only=True)
class APIConfig(BaseConfig):
api_key: str # Must be passed as keyword argument
timeout: int = 30
config = APIConfig(api_key="secret_key_123") # OK
# config = APIConfig("secret_key_123") # TypeErrorslots=True : efficacité mémoire (Python 3.10+)
Python 3.10 a ajouté slots=True pour définir __slots__, réduisant le surcoût mémoire :
from dataclasses import dataclass
import sys
@dataclass
class RegularUser:
username: str
email: str
age: int
@dataclass(slots=True)
class SlottedUser:
username: str
email: str
age: int
regular = RegularUser("john", "john@example.com", 30)
slotted = SlottedUser("jane", "jane@example.com", 28)
print(f"Regular: {sys.getsizeof(regular.__dict__)} bytes") # ~104 bytes
print(f"Slotted: {sys.getsizeof(slotted)} bytes") # ~64 bytesLes dataclasses avec slots offrent 30–40% d’économies de mémoire et un accès plus rapide aux attributs, mais au prix de l’impossibilité d’ajouter des attributs dynamiquement :
regular.new_attribute = "allowed" # OK
# slotted.new_attribute = "error" # AttributeErrorkw_only=True : champs keyword-only (Python 3.10+)
Forcer tous les champs à être keyword-only pour une instanciation plus explicite :
from dataclasses import dataclass
@dataclass(kw_only=True)
class DatabaseConnection:
host: str
port: int
username: str
password: str
database: str = "default"
# Must use keyword arguments
conn = DatabaseConnection(
host="localhost",
port=5432,
username="admin",
password="secret"
)
# Positional arguments raise TypeError
# conn = DatabaseConnection("localhost", 5432, "admin", "secret")Combiner kw_only avec un contrôle champ par champ :
from dataclasses import dataclass, field
@dataclass
class MixedArgs:
required_positional: str
optional_positional: int = 0
required_keyword: str = field(kw_only=True)
optional_keyword: bool = field(default=False, kw_only=True)
obj = MixedArgs("value", 10, required_keyword="kw_value")Comparaison : dataclass vs alternatives
| Fonctionnalité | dataclass | namedtuple | TypedDict | Pydantic | attrs |
|---|---|---|---|---|---|
| Mutabilité | Mutable (par défaut) | Immuable | N/A (sous-classe de dict) | Mutable | Configurable |
| Validation de types | Annotations uniquement | Non | Annotations uniquement | Validation à l’exécution | Validation à l’exécution |
| Valeurs par défaut | Oui | Oui | Non | Oui | Oui |
| Méthodes | Support complet de classe | Limité | Non | Support complet de classe | Support complet de classe |
| Héritage | Oui | Non | Limité | Oui | Oui |
| Surcoût mémoire | Modéré | Faible | Faible | Plus élevé | Modéré |
| Support des slots | Oui (3.10+) | Non | Non | Oui | Oui |
| Performance | Rapide | La plus rapide | Rapide | Plus lente (validation) | Rapide |
| Inclus dans Python | Oui (3.7+) | Oui | Oui (3.8+) | Non | Non |
Choisissez les dataclasses pour :
- Des projets Python standards sans dépendances
- Des conteneurs de données simples avec type hints
- Quand la flexibilité frozen/mutable est nécessaire
- Des hiérarchies d’héritage
Choisissez Pydantic pour :
- La validation des requêtes/réponses API
- La gestion de configuration avec validation stricte
- La génération de JSON schema
Choisissez namedtuple pour :
- Des conteneurs immuables légers
- Une efficacité mémoire maximale
- La compatibilité Python < 3.7
Conversion vers/depuis des dictionnaires
Les dataclasses fournissent asdict() et astuple() pour la sérialisation :
from dataclasses import dataclass, asdict, astuple
@dataclass
class Config:
host: str
port: int
ssl_enabled: bool = True
config = Config("api.example.com", 443)
# Convert to dictionary
config_dict = asdict(config)
print(config_dict) # {'host': 'api.example.com', 'port': 443, 'ssl_enabled': True}
# Convert to tuple
config_tuple = astuple(config)
print(config_tuple) # ('api.example.com', 443, True)Pour des dataclasses imbriquées :
from dataclasses import dataclass, asdict
@dataclass
class Address:
street: str
city: str
zipcode: str
@dataclass
class Person:
name: str
address: Address
person = Person("Alice", Address("123 Main St", "Springfield", "12345"))
person_dict = asdict(person)
print(person_dict)
# {'name': 'Alice', 'address': {'street': '123 Main St', 'city': 'Springfield', 'zipcode': '12345'}}Dataclasses et sérialisation JSON
Les dataclasses ne prennent pas en charge nativement la sérialisation JSON, mais l’intégration est simple :
import json
from dataclasses import dataclass, asdict
from datetime import datetime
@dataclass
class Event:
name: str
timestamp: datetime
attendees: int
def to_json(self):
data = asdict(self)
# Custom serialization for datetime
data['timestamp'] = self.timestamp.isoformat()
return json.dumps(data)
@classmethod
def from_json(cls, json_str):
data = json.loads(json_str)
data['timestamp'] = datetime.fromisoformat(data['timestamp'])
return cls(**data)
event = Event("Python Conference", datetime.now(), 500)
json_str = event.to_json()
print(json_str)
restored = Event.from_json(json_str)
print(restored)Pour des cas plus complexes, utilisez la bibliothèque dataclasses-json ou Pydantic.
Patterns du monde réel
Objets de configuration
from dataclasses import dataclass, field
from typing import List
@dataclass
class AppConfig:
app_name: str
version: str
debug: bool = False
allowed_hosts: List[str] = field(default_factory=lambda: ["localhost"])
database_url: str = "sqlite:///app.db"
cache_timeout: int = 300
def __post_init__(self):
if self.debug:
print(f"Running {self.app_name} v{self.version} in DEBUG mode")
config = AppConfig("DataAnalyzer", "2.1.0", debug=True)Modèles de réponse d’API
from dataclasses import dataclass
from typing import List, Optional
from datetime import datetime
@dataclass
class APIResponse:
status: str
data: Optional[List[dict]] = None
error_message: Optional[str] = None
timestamp: datetime = field(default_factory=datetime.now)
@property
def is_success(self):
return self.status == "success"
response = APIResponse("success", data=[{"id": 1, "name": "Dataset A"}])
print(response.is_success) # TrueEnregistrements de base de données avec intégration PyGWalker
from dataclasses import dataclass, asdict
from typing import List
import pandas as pd
@dataclass
class SalesRecord:
date: str
product: str
revenue: float
region: str
quantity: int
# Create sample data
records = [
SalesRecord("2026-01-01", "Laptop", 1299.99, "US", 5),
SalesRecord("2026-01-02", "Mouse", 29.99, "EU", 50),
SalesRecord("2026-01-03", "Keyboard", 89.99, "US", 20),
]
# Convert to DataFrame for visualization with PyGWalker
df = pd.DataFrame([asdict(r) for r in records])
# Use PyGWalker for interactive data exploration
# import pygwalker as pyg
# walker = pyg.walk(df)
# This creates a Tableau-like interface to visualize your dataclass-based dataLes dataclasses excellent pour structurer les données avant la visualisation. PyGWalker convertit des DataFrames en interfaces visuelles interactives, rendant les workflows d’analyse basés sur des dataclasses fluides.
Benchmarks de performance vs classes classiques
import timeit
from dataclasses import dataclass
# Regular class
class RegularClass:
def __init__(self, x, y, z):
self.x = x
self.y = y
self.z = z
def __repr__(self):
return f"RegularClass(x={self.x}, y={self.y}, z={self.z})"
def __eq__(self, other):
return (self.x, self.y, self.z) == (other.x, other.y, other.z)
@dataclass
class DataClass:
x: int
y: int
z: int
# Benchmark instantiation
regular_time = timeit.timeit(lambda: RegularClass(1, 2, 3), number=1000000)
dataclass_time = timeit.timeit(lambda: DataClass(1, 2, 3), number=1000000)
print(f"Regular class: {regular_time:.4f}s")
print(f"Dataclass: {dataclass_time:.4f}s")
# Dataclasses are typically 5-10% slower due to decorator overhead
# but provide significantly cleaner codeAvec slots=True (Python 3.10+), les dataclasses égalent ou dépassent les performances des classes classiques tout en réduisant l’usage mémoire de 30–40%.
Patterns avancés : ordre de champs personnalisé
from dataclasses import dataclass, field
def sort_by_priority(items):
return sorted(items, key=lambda x: x.priority, reverse=True)
@dataclass(order=True)
class Task:
priority: int
name: str = field(compare=False)
description: str = field(compare=False)
tasks = [
Task(3, "Review PR", "Code review for feature X"),
Task(1, "Write docs", "Documentation update"),
Task(5, "Fix bug", "Critical production issue"),
]
sorted_tasks = sorted(tasks)
for task in sorted_tasks:
print(f"Priority {task.priority}: {task.name}")
# Priority 1: Write docs
# Priority 3: Review PR
# Priority 5: Fix bugBonnes pratiques et pièges fréquents
- Utilisez toujours
default_factorypour les valeurs mutables : n’assignez jamais[]ou{}directement - Les type hints sont obligatoires : les dataclasses s’appuient sur les annotations, pas sur les valeurs
- L’ordre des champs compte : champs sans défaut avant champs avec défaut
frozen=Truepour les données immuables : utile pour des objets hashables et la sûreté en multi-thread- Utilisez
__post_init__avec parcimonie : trop de logique contredit la simplicité des dataclasses - Envisagez
slots=Truepour de gros datasets : économies mémoire significatives en Python 3.10+ - Validez dans
__post_init__: les dataclasses n’imposent pas les types à l’exécution
FAQ
Conclusion
Les dataclasses Python éliminent le boilerplate tout en préservant toute la puissance des classes. Le décorateur @dataclass génère automatiquement les méthodes d’initialisation, de représentation et de comparaison, réduisant le temps de développement et la charge de maintenance. Des objets de configuration aux modèles d’API et aux enregistrements de base de données, les dataclasses offrent une approche propre et annotée pour les classes porteuses de données.
Les avantages clés incluent la génération automatique de méthodes, un comportement de champ personnalisable via field(), l’immutabilité avec frozen=True, la validation via __post_init__, et une meilleure efficacité mémoire avec slots=True. Bien que des alternatives comme namedtuple et Pydantic répondent à des besoins spécifiques, les dataclasses constituent, pour la plupart des projets Python, un excellent équilibre entre simplicité et fonctionnalités.
Pour les workflows de data analysis, combiner des dataclasses avec des outils comme PyGWalker permet de construire des pipelines puissants où des modèles de données structurés alimentent directement des visualisations interactives, simplifiant tout le parcours — de l’ingestion des données à la génération d’insights.