Aller au contenu
Développement medium

Dataclasses Python : classes de données sans boilerplate

15 min de lecture

logo python

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.

  • 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=True et calculer des champs dans __post_init__.
  • Rendre vos objets comparables et triables avec order=True.
  • Convertir une dataclass en dictionnaire ou en tuple avec asdict et astuple.
  • Choisir entre une dataclass, un dict, un NamedTuple et Pydantic.

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
@dataclass
class 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.0

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

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
@dataclass
class Mauvais:
articles: list = []
ValueError: mutable default <class 'list'> for field articles is not allowed: use default_factory

La 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
@dataclass
class 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}).

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.0
Point(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.

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
@dataclass
class 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.

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).

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
@dataclass
class 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.

@dataclass
class 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}]}

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

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

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.

OutilQuand le choisirLimite principale
dictDonnées passagères, clés dynamiques inconnues à l'écritureAucune structure garantie, fautes de frappe silencieuses
NamedTuplePetit enregistrement immuable et itérable comme un tuplePas de logique métier, immuabilité obligatoire
@dataclassObjet métier structuré, mutable ou non, avec méthodesNe valide pas les types à l'exécution
PydanticDonné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.

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

6 questions
6 min.
70% requis

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

  • @dataclass gé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=True rend l'objet immuable et hachable : utilisable comme clé de dict ou dans un set.
  • __post_init__ calcule les champs dérivés et valide les données après construction.
  • order=True ajoute les comparaisons, dans l'ordre de déclaration des attributs.
  • asdict et astuple convertissent l'objet, de façon récursive, pour la sérialisation.
  • slots=True ré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.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn