Pydantic : valider des données en Python à partir de leurs types

Vos annotations de type ne vérifient rien à l'exécution. Pydantic répare cet oubli en contrôlant chaque donnée qui entre dans un programme Python.
5 min de lecture
Believemy logo

Définition

Écrire age: int dans une classe Python ne protège de rien à l'exécution : rien n'empêche un appelant de fournir la chaîne "trente" à la place d'un entier. Le problème devient concret dès qu'une donnée arrive de l'extérieur, une réponse d'API, un formulaire, un fichier, car c'est là qu'elle a le plus de chances d'être fausse. Pydantic comble cet écart : cette bibliothèque tierce transforme une annotation de type en un contrôle exécuté au moment où la donnée entre dans le programme.

Vous décrivez la forme attendue comme une classe, Pydantic reçoit un dictionnaire brut, et il rend l'un de deux résultats : un objet dont chaque champ porte le bon type, ou une erreur qui nomme le champ fautif et la raison du refus. L'exemple suivant construit un client à partir d'un identifiant transmis en texte.

PYTHON
from pydantic import BaseModel

class Client(BaseModel):
    identifiant: int
    email: str
    actif: bool = True

# "42" arrive en texte, comme depuis un formulaire
client = Client(identifiant="42", email="marie@exemple.fr")
print(client.identifiant)   # 42, devenu un entier

L'installation passe par pip, comme pour toute bibliothèque hors de la bibliothèque standard.

BASH
pip install pydantic


Le problème qu'il résout

Le vrai souci n'est pas que Python autorise ce genre d'erreur, c'est qu'aucun outil ne la voit venir au bon moment. Un vérificateur statique comme mypy attrape bien la contradiction dans le code que vous écrivez, mais il ne voit jamais ce qui arrive de l'extérieur : une réponse d'API, un fichier json, un formulaire, une ligne de base de données.

Voici à quoi ressemble ce contrôle écrit à la main, pour les deux premiers champs d'un objet à peine plus riche que l'exemple précédent.

PYTHON
if "identifiant" not in donnees:
    raise ValueError("identifiant manquant")
if not isinstance(donnees["identifiant"], int):
    raise TypeError("identifiant doit être un entier")
if "email" not in donnees:
    raise ValueError("email manquant")

Ce bloc fonctionne, mais il double la taille de la fonction pour deux champs à peine, et un champ qui change le rend faux. Pydantic remplace tout cela par la déclaration du modèle : la description de la forme attendue devient le contrôle, et les ValueError ou TypeError écrites une par une n'ont plus de raison d'exister.


Pydantic ou dataclass

Une question revient vite : pourquoi ne pas se contenter d'une dataclass, déjà fournie par Python ? Elle écrit à votre place le constructeur, l'affichage et l'égalité d'un objet de données, mais elle ne regarde jamais les valeurs qu'elle reçoit. Une dataclass accepte "vingt" dans un champ censé contenir un entier aussi volontiers qu'elle accepte 20.

Le tableau suivant reprend les différences qui comptent pour choisir entre les deux.

BesoindataclassPydantic
Éviter le code répétitifOuiOui
Vérifier les types à l'exécutionNonOui
Message d'erreur par champNonOui
Relire une structure imbriquéeÀ la mainAutomatique
Fourni avec PythonOuiNon

Une dataclass convient à un objet que votre code fabrique lui-même ; un modèle Pydantic prend le relais pour un objet venu de données que vous n'avez pas écrites.


Les trois surprises du début

Trois comportements surprennent presque toujours celui qui découvre Pydantic, et mieux vaut les connaître avant d'y tomber plutôt qu'après.

Attention

Par défaut, la chaîne "42" placée dans un champ int ne déclenche aucun refus : elle devient l'entier 42. Ce comportement rend service sur un formulaire, où tout arrive en texte, mais il surprend qui attend un contrôle strict. Le mode strict existe, mais il se demande explicitement, champ par champ ou modèle par modèle.

La deuxième surprise touche au moment où la vérification a lieu : à la construction de l'objet, et nulle part ailleurs. Modifier un attribut après coup ne repasse par aucun contrôle tant que la validation à l'affectation n'a pas été activée à part, si bien qu'un objet Pydantic n'est pas surveillé en permanence : il est vérifié une fois.

La troisième tient à la version installée. Le passage de la 1 à la 2 a renommé l'essentiel de l'interface publique, et beaucoup d'exemples en ligne parlent encore l'ancienne langue. Le tableau suivant fait correspondre les deux vocabulaires.

Pydantic 1Pydantic 2
.dict().model_dump()
.json().model_dump_json()
parse_obj()model_validate()
@validator@field_validator


Quand il ne sert à rien

Sur des données que votre programme vient lui-même de produire, valider n'apporte rien : vérifier deux fois ce que l'on a écrit soi-même coûte du temps sans jamais attraper d'erreur. Dans une boucle qui construit des centaines de milliers d'objets, ce coût devient mesurable, et une dataclass ordinaire reprend l'avantage.

Il ne remplace pas non plus un vérificateur statique, qui travaille avant le lancement sur du code que Pydantic ne voit jamais, ni les tests : un modèle respecté peut décrire un prix négatif si personne n'a demandé qu'il soit positif. Valider une forme n'est pas valider une règle métier, à moins d'écrire cette règle explicitement dans le modèle.


Questions fréquentes

Question

Pydantic remplace-t-il mypy ?

Non, les deux travaillent à des moments différents. mypy lit le code avant l'exécution ; Pydantic lit des valeurs pendant l'exécution et refuse celles qui ne correspondent pas. Les projets sérieux emploient les deux, sur les mêmes annotations.

Question

Comment récupérer les erreurs au lieu de laisser planter le programme ?

En entourant la construction du modèle d'un bloc try, et en interceptant ValidationError avec un except. Cette exception expose une méthode errors() qui rend la liste des champs refusés, chacun avec son chemin et son motif : de quoi construire une réponse lisible plutôt qu'une trace de plantage.

Question

Faut-il un modèle pour chaque dictionnaire du projet ?

Non, et c'est l'excès le plus courant chez ceux qui découvrent la bibliothèque. Un modèle se pose aux entrées et aux sorties du programme, là où les données changent de main ; au milieu, le code travaille sur des objets déjà validés. D'où sa place dans FastAPI, où le modèle sert à la fois de contrôle et de documentation de l'interface.

Termes connexes

Découvrez notre glossaire Python

Parcourez les termes et définitions les plus couramment utilisés dans le domaine du développement avec Python.

Partager cet article

Tu veux nous aider ? Fais un lien vers cet article sur tes réseaux ou encore mieux : sur ton site, dans un article ou dans ta newsletter.