Skip to content

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: int

Le 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)  # True

Le 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: int

Types 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 list

La 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_012345

Principaux paramètres de field() :

ParamètreTypeDescription
defaultAnyValeur par défaut du champ
default_factoryCallableFonction sans argument renvoyant la valeur par défaut
initboolInclure le champ dans __init__ (par défaut : True)
reprboolInclure le champ dans __repr__ (par défaut : True)
compareboolInclure le champ dans les méthodes de comparaison (par défaut : True)
hashboolInclure le champ dans __hash__ (par défaut : None)
metadatadictMétadonnées arbitraires (non utilisées par le module dataclasses)
kw_onlyboolRendre 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 York

Mé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)  # Gold

Pour 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.0

Hé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 = 30

Python 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")  # TypeError

slots=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 bytes

Les 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"  # AttributeError

kw_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édataclassnamedtupleTypedDictPydanticattrs
MutabilitéMutable (par défaut)ImmuableN/A (sous-classe de dict)MutableConfigurable
Validation de typesAnnotations uniquementNonAnnotations uniquementValidation à l’exécutionValidation à l’exécution
Valeurs par défautOuiOuiNonOuiOui
MéthodesSupport complet de classeLimitéNonSupport complet de classeSupport complet de classe
HéritageOuiNonLimitéOuiOui
Surcoût mémoireModéréFaibleFaiblePlus élevéModéré
Support des slotsOui (3.10+)NonNonOuiOui
PerformanceRapideLa plus rapideRapidePlus lente (validation)Rapide
Inclus dans PythonOui (3.7+)OuiOui (3.8+)NonNon

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)  # True

Enregistrements 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 data

Les 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 code

Avec 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 bug

Bonnes pratiques et pièges fréquents

  1. Utilisez toujours default_factory pour les valeurs mutables : n’assignez jamais [] ou {} directement
  2. Les type hints sont obligatoires : les dataclasses s’appuient sur les annotations, pas sur les valeurs
  3. L’ordre des champs compte : champs sans défaut avant champs avec défaut
  4. frozen=True pour les données immuables : utile pour des objets hashables et la sûreté en multi-thread
  5. Utilisez __post_init__ avec parcimonie : trop de logique contredit la simplicité des dataclasses
  6. Envisagez slots=True pour de gros datasets : économies mémoire significatives en Python 3.10+
  7. 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.

📚