2 nov. 2026

Pydantic vs dataclasses : lequel choisir ?

Développement logiciel

Logo invivoo.

Grow

Together

man using laptop

2 nov. 2026

Pydantic vs dataclasses : lequel choisir ?

Développement logiciel

Logo invivoo.

Grow

Together

man using laptop

2 nov. 2026

Pydantic vs dataclasses : lequel choisir ?

Développement logiciel

Logo invivoo.

Grow

Together

man using laptop

La réponse courte : utilisez les dataclasses pour structurer des données que votre propre code produit et Pydantic dès que les données viennent de l'extérieur (API, fichier, formulaire, variable d'environnement). Les dataclasses décrivent la forme des données ; Pydantic vérifie qu'elles la respectent vraiment.

Cet article compare les deux approches avec des exemples de code, puis donne des règles simples pour choisir, et même les combiner. Il fait suite à notre article sur les dataclasses Python.

Les dataclasses en bref

Les dataclasses font partie de la bibliothèque standard depuis Python 3.7. Le décorateur @dataclass génère automatiquement __init__, __repr__ et __eq__ à partir des annotations de type.

from dataclasses import dataclass

@dataclass
class Trade:
    isin: str
    quantity: int
    price: float

trade = Trade(isin="FR0000120271", quantity="100", price=52.3)
print(trade.quantity)        # "100" : une chaîne, pas un entier
from dataclasses import dataclass

@dataclass
class Trade:
    isin: str
    quantity: int
    price: float

trade = Trade(isin="FR0000120271", quantity="100", price=52.3)
print(trade.quantity)        # "100" : une chaîne, pas un entier
from dataclasses import dataclass

@dataclass
class Trade:
    isin: str
    quantity: int
    price: float

trade = Trade(isin="FR0000120271", quantity="100", price=52.3)
print(trade.quantity)        # "100" : une chaîne, pas un entier
from dataclasses import dataclass

@dataclass
class Trade:
    isin: str
    quantity: int
    price: float

trade = Trade(isin="FR0000120271", quantity="100", price=52.3)
print(trade.quantity)        # "100" : une chaîne, pas un entier

L'exemple montre la limite principale : les annotations ne sont pas vérifiées à l'exécution. Python accepte la chaîne "100" là où l'on attend un entier. Seul un outil d'analyse statique comme mypy signalera l'erreur, et seulement si l'appel est écrit en dur dans le code.

En contrepartie, les dataclasses sont légères, sans dépendance, et offrent des options utiles : frozen=True pour l'immuabilité, slots=True pour réduire la mémoire, order=True pour la comparaison.

Pydantic en bref

Pydantic est une bibliothèque tierce (pip install pydantic) qui valide et convertit les données à l'exécution à partir des mêmes annotations de type. Depuis la version 2, son moteur de validation, pydantic-core, est écrit en Rust. La dernière version stable est la 2.13, et elle demande Python 3.10 ou plus récent.

from pydantic import BaseModel, Field, ValidationError

class Trade(BaseModel):
    isin: str = Field(min_length=12, max_length=12)
    quantity: int = Field(gt=0)
    price: float

trade = Trade(isin="FR0000120271", quantity="100", price=52.3)
print(trade.quantity)        # 100 : converti en entier

try:
    Trade(isin="FR00", quantity=-5, price="abc")
except ValidationError as e:
    print(e.error_count())   # 3 erreurs, une par champ

print(trade.model_dump_json())
# {"isin":"FR0000120271","quantity":100,"price":52.3}
from pydantic import BaseModel, Field, ValidationError

class Trade(BaseModel):
    isin: str = Field(min_length=12, max_length=12)
    quantity: int = Field(gt=0)
    price: float

trade = Trade(isin="FR0000120271", quantity="100", price=52.3)
print(trade.quantity)        # 100 : converti en entier

try:
    Trade(isin="FR00", quantity=-5, price="abc")
except ValidationError as e:
    print(e.error_count())   # 3 erreurs, une par champ

print(trade.model_dump_json())
# {"isin":"FR0000120271","quantity":100,"price":52.3}
from pydantic import BaseModel, Field, ValidationError

class Trade(BaseModel):
    isin: str = Field(min_length=12, max_length=12)
    quantity: int = Field(gt=0)
    price: float

trade = Trade(isin="FR0000120271", quantity="100", price=52.3)
print(trade.quantity)        # 100 : converti en entier

try:
    Trade(isin="FR00", quantity=-5, price="abc")
except ValidationError as e:
    print(e.error_count())   # 3 erreurs, une par champ

print(trade.model_dump_json())
# {"isin":"FR0000120271","quantity":100,"price":52.3}
from pydantic import BaseModel, Field, ValidationError

class Trade(BaseModel):
    isin: str = Field(min_length=12, max_length=12)
    quantity: int = Field(gt=0)
    price: float

trade = Trade(isin="FR0000120271", quantity="100", price=52.3)
print(trade.quantity)        # 100 : converti en entier

try:
    Trade(isin="FR00", quantity=-5, price="abc")
except ValidationError as e:
    print(e.error_count())   # 3 erreurs, une par champ

print(trade.model_dump_json())
# {"isin":"FR0000120271","quantity":100,"price":52.3}

En quelques lignes, on obtient la conversion de type, des contraintes métier, des messages d'erreur détaillés et la sérialisation JSON. Pydantic génère aussi un JSON Schema, ce qui explique sa place centrale dans FastAPI et dans les frameworks d'agents IA qui structurent les réponses des LLM.

Comparatif point par point

Critère

dataclasses

Pydantic

Installation

Bibliothèque standard

Dépendance externe

Validation à l'exécution

Non

Oui

Conversion de types

Non

Oui ("100" devient 100), désactivable en mode strict

Contraintes (bornes, longueur, regex)

À coder dans __post_init__

Déclaratives avec Field()

Sérialisation JSON

asdict() puis json.dumps()

model_dump_json() intégré

JSON Schema

Non

Oui

Immuabilité

frozen=True

model_config = ConfigDict(frozen=True)

Coût à la création d'objet

Minimal

Coût de la validation

Écosystème

Universel

FastAPI, pydantic-settings, frameworks d'agents IA

Performances : ce que disent les mesures

Créer une dataclass est environ 6 à 8 fois plus rapide que créer un objet Pydantic, car il n'y a rien à valider. Nous avons mesuré la création de l'objet Trade à trois champs (Python 3.13, Pydantic 2.13, meilleur temps sur 5 séries de 200 000 créations) :

Approche

Temps par objet

dataclasses.dataclass

101 ns

pydantic.dataclasses.dataclass

622 ns

pydantic.BaseModel

830 ns

Ces écarts sont réels mais rarement décisifs : moins d'une microseconde par objet, c'est négligeable face à un appel réseau ou une requête SQL. Ils comptent en revanche dans une boucle de calcul qui crée des millions d'objets, par exemple un moteur de pricing. Mesurez sur votre propre cas avec timeit avant de trancher.

import timeit
print(timeit.timeit(lambda: Trade(isin="FR0000120271", quantity=100, price=52.3), number=200_000))
import timeit
print(timeit.timeit(lambda: Trade(isin="FR0000120271", quantity=100, price=52.3), number=200_000))
import timeit
print(timeit.timeit(lambda: Trade(isin="FR0000120271", quantity=100, price=52.3), number=200_000))
import timeit
print(timeit.timeit(lambda: Trade(isin="FR0000120271", quantity=100, price=52.3), number=200_000))

Quand choisir l'un ou l'autre

La bonne question n'est pas « lequel est le meilleur ? » mais « d'où viennent mes données ? ».

Choisissez Pydantic quand :

  • les données arrivent de l'extérieur : requête HTTP, fichier CSV ou JSON, message Kafka, réponse d'une API tierce ;

  • vous développez une API avec FastAPI ;

  • vous chargez une configuration depuis des variables d'environnement (avec pydantic-settings) ;

  • vous demandez à un LLM une réponse structurée et devez vérifier qu'elle respecte le schéma attendu.

Choisissez les dataclasses quand :

  • les objets sont créés par votre propre code, à partir de données déjà validées ;

  • vous écrivez une bibliothèque et voulez éviter d'imposer une dépendance ;

  • la performance compte dans une boucle critique ;

  • vous modélisez le cœur du domaine métier, que vous voulez garder indépendant des frameworks.

Dans une architecture propre, les deux cohabitent souvent : Pydantic à la frontière du système pour filtrer les entrées, des dataclasses à l'intérieur pour le domaine métier.

Combiner les deux

Pas besoin de choisir une fois pour toutes. Pydantic propose deux passerelles vers les dataclasses.

1. Le décorateur pydantic.dataclasses.dataclass. Il s'utilise comme le décorateur standard mais ajoute la validation. C'est la solution idéale pour durcir une dataclass existante sans la réécrire.

from pydantic.dataclasses import dataclass

@dataclass
class Trade:
    isin: str
    quantity: int
    price: float

Trade(isin="FR0000120271", quantity="100", price=52.3).quantity   # 100
from pydantic.dataclasses import dataclass

@dataclass
class Trade:
    isin: str
    quantity: int
    price: float

Trade(isin="FR0000120271", quantity="100", price=52.3).quantity   # 100
from pydantic.dataclasses import dataclass

@dataclass
class Trade:
    isin: str
    quantity: int
    price: float

Trade(isin="FR0000120271", quantity="100", price=52.3).quantity   # 100
from pydantic.dataclasses import dataclass

@dataclass
class Trade:
    isin: str
    quantity: int
    price: float

Trade(isin="FR0000120271", quantity="100", price=52.3).quantity   # 100

2. TypeAdapter, pour valider une dataclass standard à la frontière. Le domaine métier garde ses dataclasses pures, et la validation n'a lieu qu'au point d'entrée.

from dataclasses import dataclass
from pydantic import TypeAdapter

@dataclass
class Trade:
    isin: str
    quantity: int

adapter = TypeAdapter(Trade)
trade = adapter.validate_json('{"isin": "FR0000120271", "quantity": "100"}')
print(trade)   # Trade(isin='FR0000120271', quantity=100)
from dataclasses import dataclass
from pydantic import TypeAdapter

@dataclass
class Trade:
    isin: str
    quantity: int

adapter = TypeAdapter(Trade)
trade = adapter.validate_json('{"isin": "FR0000120271", "quantity": "100"}')
print(trade)   # Trade(isin='FR0000120271', quantity=100)
from dataclasses import dataclass
from pydantic import TypeAdapter

@dataclass
class Trade:
    isin: str
    quantity: int

adapter = TypeAdapter(Trade)
trade = adapter.validate_json('{"isin": "FR0000120271", "quantity": "100"}')
print(trade)   # Trade(isin='FR0000120271', quantity=100)
from dataclasses import dataclass
from pydantic import TypeAdapter

@dataclass
class Trade:
    isin: str
    quantity: int

adapter = TypeAdapter(Trade)
trade = adapter.validate_json('{"isin": "FR0000120271", "quantity": "100"}')
print(trade)   # Trade(isin='FR0000120271', quantity=100)

Attention : les dataclasses Pydantic n'ont pas toutes les méthodes de BaseModel (model_dump, model_json_schema…). Pour sérialiser, passez aussi par TypeAdapter(Trade).dump_python(trade).

Pour aller plus loin