
Une dataclass est une classe conçue pour stocker des données, dont Python génère automatiquement le code répétitif : le constructeur __init__, l'affichage __repr__ et la comparaison __eq__. Vous décrivez juste les attributs typés, le décorateur @dataclass s'occupe du reste. Fini les constructeurs de dix lignes qui recopient chaque paramètre dans self. Ce guide s'adresse aux développeurs qui manipulent des structures de données (configuration, objets métier, résultats d'API) et veulent un code plus court et plus lisible. Il couvre les valeurs par défaut, field(default_factory=...), l'immuabilité avec frozen=True, le hook __post_init__, les comparaisons avec order=True, la conversion avec asdict, l'optimisation slots=True, et le choix entre dataclass, dict, NamedTuple et Pydantic. Les exemples ont été exécutés avec Python 3.12.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Générer un constructeur et un affichage lisible avec
@dataclass. - Donner des valeurs par défaut sûres, y compris pour les listes, avec
field(default_factory=...). - Créer des objets immuables avec
frozen=Trueet calculer des champs dans__post_init__. - Rendre vos objets comparables et triables avec
order=True. - Convertir une dataclass en dictionnaire ou en tuple avec
asdictetastuple. - Choisir entre une dataclass, un
dict, unNamedTupleet Pydantic.
Qu'est-ce qu'une dataclass
Section intitulée « Qu'est-ce qu'une dataclass »Le module dataclasses fait partie de la bibliothèque standard depuis Python 3.7 : rien à installer. Le décorateur @dataclass transforme une classe en générant pour vous les méthodes courantes à partir des attributs annotés. Chaque attribut doit porter un type hint, car c'est cette annotation que le décorateur inspecte pour construire le constructeur.
from dataclasses import dataclass
@dataclassclass Article: reference: str prix: float quantite: int = 0
stylo = Article(reference="STY-01", prix=1.5, quantite=100)print(stylo)print(stylo.prix * stylo.quantite)La sortie montre un __repr__ lisible généré automatiquement, sans une seule ligne de code d'affichage :
Article(reference='STY-01', prix=1.5, quantite=100)150.0Le gain saute aux yeux quand on écrit la même chose à la main. Sans dataclass, il faut un constructeur qui recopie chaque paramètre, et l'affichage par défaut reste illisible :
class ArticleClassique: def __init__(self, reference, prix, quantite=0): self.reference = reference self.prix = prix self.quantite = quantite
print(ArticleClassique("STY-01", 1.5, 100))<__main__.ArticleClassique object at 0x786bc7f372f0>Autre bénéfice majeur : la comparaison par valeur. Deux dataclasses ayant les mêmes attributs sont égales, là où deux objets classiques sont différents par défaut (comparaison par identité).
print(Article("STY-01", 1.5, 100) == Article("STY-01", 1.5, 100))print(ArticleClassique("STY-01", 1.5, 100) == ArticleClassique("STY-01", 1.5, 100))TrueFalseValeurs par défaut et le piège des listes
Section intitulée « Valeurs par défaut et le piège des listes »Comme pour n'importe quelle fonction, un attribut peut recevoir une valeur par défaut directement après son type. C'est ce qu'on a fait avec quantite: int = 0. Cela fonctionne pour tous les types immuables : nombres, chaînes, booléens, None.
Le problème surgit avec les types mutables comme les listes ou les dictionnaires. Si vous écriviez articles: list = [], la liste serait créée une seule fois et partagée entre toutes les instances : modifier le panier d'un client modifierait celui de tous les autres. Python refuse d'ailleurs cette écriture et lève une ValueError explicite.
from dataclasses import dataclass
@dataclassclass Mauvais: articles: list = []ValueError: mutable default <class 'list'> for field articles is not allowed: use default_factoryLa solution est field(default_factory=...) : vous passez une fonction (ici list) que Python appelle pour créer une nouvelle valeur à chaque instance. Chaque objet obtient ainsi sa propre liste.
from dataclasses import dataclass, field
@dataclassclass Panier: client: str articles: list[str] = field(default_factory=list)
p1 = Panier(client="Alice")p2 = Panier(client="Bob")p1.articles.append("STY-01")print("p1:", p1)print("p2:", p2)La liste d'Alice reçoit l'article, celle de Bob reste vide : les deux paniers sont bien indépendants.
p1: Panier(client='Alice', articles=['STY-01'])p2: Panier(client='Bob', articles=[])Retenez la règle : tout défaut mutable (liste, dictionnaire, ensemble) passe par default_factory. Vous pouvez y mettre n'importe quel appelable, y compris une lambda pour un dictionnaire pré-rempli : field(default_factory=lambda: {"actif": True}).
Immuabilité avec frozen
Section intitulée « Immuabilité avec frozen »Par défaut, les attributs d'une dataclass sont modifiables après création. Pour beaucoup d'objets métier, c'est risqué : un point géographique, une version ou une clé de configuration ne devraient jamais changer une fois construits. Le paramètre frozen=True rend l'instance immuable : toute tentative de modification lève une FrozenInstanceError.
from dataclasses import dataclass
@dataclass(frozen=True)class Point: x: float y: float
origine = Point(0.0, 0.0)print(origine)origine.x = 10.0Point(x=0.0, y=0.0)FrozenInstanceError: cannot assign to field 'x'L'immuabilité apporte un second avantage précieux : une dataclass frozen devient hachable. Elle peut donc servir de clé de dictionnaire ou entrer dans un ensemble (set), ce qui déduplique naturellement les valeurs identiques.
print({Point(1.0, 2.0), Point(1.0, 2.0)}){Point(x=1.0, y=2.0)}Les deux points identiques sont fusionnés en un seul dans l'ensemble, exactement comme le seraient deux chaînes égales.
Calculer des champs avec post_init
Section intitulée « Calculer des champs avec post_init »Parfois, un attribut n'est pas fourni à la construction mais dérivé des autres : un total calculé à partir d'un prix et d'une quantité, un nom complet à partir d'un prénom et d'un nom. Le hook __post_init__ est une méthode appelée automatiquement juste après le constructeur généré. C'est l'endroit idéal pour ces calculs ou pour valider les données reçues.
On combine ici deux outils : field(init=False) retire le champ des paramètres du constructeur (il ne sera pas demandé à l'appel), et __post_init__ lui donne sa valeur calculée.
from dataclasses import dataclass, field
@dataclassclass Commande: prix_unitaire: float quantite: int total: float = field(init=False)
def __post_init__(self): self.total = self.prix_unitaire * self.quantite
c = Commande(prix_unitaire=2.5, quantite=4)print(c)Le champ total est rempli tout seul, sans qu'on l'ait passé à la création :
Commande(prix_unitaire=2.5, quantite=4, total=10.0)C'est aussi le bon endroit pour lever une exception si une valeur est invalide, par exemple une quantité négative. Attention toutefois : sur une dataclass frozen, __post_init__ ne peut pas affecter un attribut par simple assignation. Ce cas plus avancé est traité dans le guide sur la POO avancée.
Trier et comparer avec order
Section intitulée « Trier et comparer avec order »Générer __eq__ suffit pour tester l'égalité, mais pas pour ordonner des objets. Le paramètre order=True ajoute les méthodes de comparaison (<, <=, >, >=). La comparaison se fait dans l'ordre de déclaration des attributs, comme si Python comparait un tuple (majeure, mineure, patch).
from dataclasses import dataclass
@dataclass(order=True)class Version: majeure: int mineure: int patch: int
versions = [Version(1, 2, 0), Version(1, 0, 5), Version(2, 0, 0)]print(sorted(versions))Le tri classe d'abord sur la version majeure, puis départage sur la mineure, enfin sur le patch :
[Version(majeure=1, mineure=0, patch=5), Version(majeure=1, mineure=2, patch=0), Version(majeure=2, mineure=0, patch=0)]L'ordre des champs compte donc énormément avec order=True. Placez toujours en premier l'attribut le plus significatif pour le tri. Si le tri naturel ne correspond pas à l'ordre de déclaration, vous pouvez exclure un champ de la comparaison avec field(compare=False).
Convertir en dict ou en tuple
Section intitulée « Convertir en dict ou en tuple »Une dataclass finit souvent par devoir sortir de votre code : sérialisée en JSON, envoyée à une API, écrite dans un fichier. Les fonctions asdict et astuple convertissent une instance en dictionnaire ou en tuple. asdict est le plus utile car son résultat se passe directement à json.dumps.
from dataclasses import dataclass, asdict, astuple
@dataclassclass Serveur: hote: str port: int
srv = Serveur(hote="db-prod", port=5432)print(asdict(srv))print(astuple(srv)){'hote': 'db-prod', 'port': 5432}('db-prod', 5432)Point fort : asdict est récursif. Si une dataclass contient d'autres dataclasses, toute l'arborescence est convertie en dictionnaires imbriqués, ce qui donne une structure prête à sérialiser sans effort.
@dataclassclass Cluster: nom: str noeuds: list[Serveur] = field(default_factory=list)
cl = Cluster(nom="prod", noeuds=[Serveur("n1", 5432), Serveur("n2", 5432)])print(asdict(cl)){'nom': 'prod', 'noeuds': [{'hote': 'n1', 'port': 5432}, {'hote': 'n2', 'port': 5432}]}Optimiser la mémoire avec slots
Section intitulée « Optimiser la mémoire avec slots »Par défaut, chaque instance Python stocke ses attributs dans un dictionnaire interne (__dict__), souple mais coûteux en mémoire. Le paramètre slots=True (disponible depuis Python 3.10) remplace ce dictionnaire par une structure figée : les instances consomment moins de mémoire et l'accès aux attributs est plus rapide. C'est un gain réel quand vous créez des milliers d'objets.
from dataclasses import dataclass
@dataclass(slots=True)class PointSlots: x: float y: float
ps = PointSlots(1.0, 2.0)print(ps)print("__slots__ =", PointSlots.__slots__)ps.z = 3.0La contrepartie est visible sur la dernière ligne : avec slots=True, on ne peut plus ajouter d'attribut hors de ceux déclarés. Toute tentative lève une AttributeError.
PointSlots(x=1.0, y=2.0)__slots__ = ('x', 'y')AttributeError: 'PointSlots' object has no attribute 'z'Cette rigidité est souvent une bonne chose : elle attrape les fautes de frappe sur les noms d'attributs. Activez slots=True par défaut sur les dataclasses créées en grande quantité, sauf si vous avez besoin d'attributs dynamiques.
Dataclass, dict, NamedTuple ou Pydantic
Section intitulée « Dataclass, dict, NamedTuple ou Pydantic »La dataclass n'est pas toujours le bon outil. Trois alternatives reviennent souvent, chacune avec un domaine de prédilection. Ce tableau résume quand choisir quoi.
| Outil | Quand le choisir | Limite principale |
|---|---|---|
dict | Données passagères, clés dynamiques inconnues à l'écriture | Aucune structure garantie, fautes de frappe silencieuses |
NamedTuple | Petit enregistrement immuable et itérable comme un tuple | Pas de logique métier, immuabilité obligatoire |
@dataclass | Objet métier structuré, mutable ou non, avec méthodes | Ne valide pas les types à l'exécution |
| Pydantic | Données externes à valider (API, config, formulaire) | Dépendance tierce à installer |
Le dict reste parfait pour du transitoire : une réponse JSON qu'on lit une fois. Mais dès qu'une structure est réutilisée, un dict devient fragile : rien n'empêche une clé mal orthographiée, et l'éditeur ne propose aucune autocomplétion. La dataclass donne un contrat clair et des attributs vérifiés par l'éditeur.
Le NamedTuple est un tuple nommé : immuable, léger, itérable. Il brille pour un petit enregistrement en lecture seule que l'on veut parfois manipuler comme un tuple.
from typing import NamedTuple
class PointNT(NamedTuple): x: float y: float
pnt = PointNT(1.0, 2.0)print(pnt, pnt.x, pnt[0])print(list(pnt))PointNT(x=1.0, y=2.0) 1.0 1.0[1.0, 2.0]On accède à un champ par nom (pnt.x) ou par index (pnt[0]), et l'objet s'itère comme un tuple. Si vous avez besoin de mutabilité, de méthodes ou d'un simple champ modifiable, la dataclass est plus adaptée.
Contrôle de connaissances
Section intitulée « Contrôle de connaissances »Vérifiez que l'essentiel de ce guide est acquis. Les questions portent uniquement sur ce qui vient d'être expliqué ici.
Contrôle de connaissances
Validez vos connaissances avec ce quiz interactif
Informations
- Le chronomètre démarre au clic sur Démarrer
- Questions à choix multiples, vrai/faux et réponses courtes
- Vous pouvez naviguer entre les questions
- Les résultats détaillés sont affichés à la fin
Lance le quiz et démarre le chronomètre
Vérification
(0/0)Profil de compétences
Quoi faire maintenant
Ressources pour progresser
Des indices pour retenter votre chance ?
Nouveau quiz complet avec des questions aléatoires
Retravailler uniquement les questions ratées
Retour à la liste des certifications
À retenir
Section intitulée « À retenir »@dataclassgénère__init__,__repr__et__eq__à partir des attributs typés : moins de code répétitif.- Deux dataclasses aux mêmes valeurs sont égales ; deux objets classiques ne le sont pas par défaut.
- Un défaut mutable (liste, dict) passe obligatoirement par
field(default_factory=...), jamais par= []. frozen=Truerend l'objet immuable et hachable : utilisable comme clé de dict ou dans unset.__post_init__calcule les champs dérivés et valide les données après construction.order=Trueajoute les comparaisons, dans l'ordre de déclaration des attributs.asdictetastupleconvertissent l'objet, de façon récursive, pour la sérialisation.slots=Trueréduit la mémoire et interdit les attributs non déclarés.- Une dataclass ne valide pas les types : pour des données externes, utiliser Pydantic.
Pour aller plus loin
Section intitulée « Pour aller plus loin »- Les type hints en Python : le socle des dataclasses : annoter variables, fonctions et collections.
- POO : les bases en Python : comprendre classes, attributs et méthodes avant d'automatiser avec dataclass.
- POO avancée en Python : aller plus loin avec frozen, slots, classes abstraites et mixins.
- Le pattern matching (match/case) : filtrer une dataclass par ses attributs avec les motifs de classe.