Cours Python · Module 6: Async et FastAPI
Pydantic - la validation de données propulsée par Rust
Dans cette leçon5
Bienvenue ! Ici Darwin. Un garde du parc a saisi la population « cent vingt » au lieu de 120, et une date dans le champ de l'espèce. Sans contrôle, une telle entrée arrive dans la base et fausse les statistiques de toute la réserve. Pydantic est une bibliothèque de validation et de sérialisation de données fondée sur les type hints : tu décris la forme des données avec des annotations de type, et elle vérifie chaque valeur.
Pydantic est le cœur de FastAPI - le framework s'en sert pour valider les paramètres et le corps des requêtes et pour façonner les réponses. Dans la version 2, la logique de validation est passée dans le paquet pydantic-core écrit en Rust, et selon ses auteurs un modèle typique se valide environ 17 fois plus vite qu'en version 1.
Analogie safari : Pydantic, c'est comme un système de contrôle qualité des observations - il vérifie que chaque observation contient des données correctes (l'espèce existe, la population est un nombre, la date est valide). Si quelque chose ne va pas, il signale une erreur immédiatement !
Les bases de Pydantic
BaseModel - la classe de base
Un modèle est une classe qui hérite de BaseModel. Chaque champ est un nom avec une annotation de type, et la valeur après = devient la valeur par défaut :
1from pydantic import BaseModel
2
3class Species(BaseModel):
4 id: int
5 name: str
6 population: int
7 endangered: bool = False # Valeur par défaut
8
9# Création d'une instance
10lion = Species(id=1, name="Lion", population=120, endangered=True)
11
12print(lion.name) # Lion
13print(lion.model_dump()) # {'id': 1, 'name': 'Lion', 'population': 120, 'endangered': True}
14print(lion.model_dump_json()) # {"id":1,"name":"Lion","population":120,"endangered":true}model_dump() transforme le modèle en dictionnaire, et model_dump_json() en texte JSON. Les anciens tutoriels utilisent .dict() et .json() - ils marchent encore dans Pydantic 2, mais émettent un avertissement de dépréciation et disparaîtront en version 3.
Les champs optionnels
Toutes les observations n'ont pas de description. Un champ qui peut rester vide reçoit le type Optional[str] et la valeur par défaut None :
1from typing import Optional
2from pydantic import BaseModel
3
4class Sighting(BaseModel):
5 species: str
6 count: int
7 description: Optional[str] = None
8
9print(Sighting(species="Lion", count=3)) # species='Lion' count=3 description=NoneOptional[str] seul permet de passer None, mais seul = None permet d'omettre le champ. Depuis Python 3.10, tu peux écrire la même chose sous la forme str | None = None.
La conversion automatique des types
Les données des formulaires et des URL arrivent sous forme de texte. Pydantic essaie de les convertir vers le type déclaré :
1# Pydantic convertit automatiquement les types !
2species = Species(id="1", name="Lion", population="120", endangered="yes")
3
4print(type(species.id)) # <class 'int'> (conversion str→int)
5print(type(species.population)) # <class 'int'>
6print(species.endangered) # True (conversion "yes"→True)La conversion n'a lieu que sans perte d'information : "120" devient un nombre, mais "abc" ou 1.5 ne passeront pas dans un champ int. Pydantic n'insère alors pas None, il lève une erreur.
Les erreurs de validation
Quand une conversion échoue, Pydantic lève une ValidationError qui liste tous les problèmes. On l'attrape comme n'importe quelle exception :
1from pydantic import ValidationError
2
3try:
4 # Données invalides
5 species = Species(id="abc", name="Lion", population=-50)
6except ValidationError as e:
7 print(e.error_count()) # 1
8 print(e.json()) # Erreurs de validation détaillées (JSON)Il n'y a qu'une erreur, car -50 est un int valide - le modèle ne connaît aucune règle interdisant une population négative. C'est Field qui ajoute ce genre de règles.
Field - la validation avancée
Field attache des contraintes à un champ : plages de nombres, longueur de texte, motif. Les trois points ... marquent un champ obligatoire :
1from pydantic import BaseModel, Field
2
3class Species(BaseModel):
4 id: int = Field(..., gt=0, description="Unique species ID")
5 name: str = Field(..., min_length=2, max_length=100)
6 scientific_name: str = Field(..., pattern=r'^[A-Z][a-z]+ [a-z]+$') # Regex
7 population: int = Field(..., ge=0, le=1_000_000) # >= 0, <= 1M
8 habitat: str = Field(default="unknown")
9
10# La validation fonctionne !
11lion = Species(
12 id=1,
13 name="Lion",
14 scientific_name="Panthera leo", # Doit correspondre à la regex
15 population=120
16)Désormais, population=-50 serait rejeté par ge=0. Field(gt=0) sans valeur par défaut désigne aussi un champ obligatoire - c'est plus court, donc je te le conseille.
Les validateurs de Field :
gt,ge- strictement supérieur, supérieur ou égallt,le- strictement inférieur, inférieur ou égalmin_length,max_length- longueur de chaînepattern- validation par regexdescription- description pour la documentation
Les validateurs personnalisés
Une règle inhabituelle s'écrit comme une fonction avec le décorateur @field_validator. Elle renvoie la valeur ou lève une ValueError :
1from pydantic import BaseModel, ValidationError, field_validator
2
3class Species(BaseModel):
4 name: str
5 population: int
6
7 @field_validator('name')
8 @classmethod
9 def name_must_be_capitalized(cls, v):
10 if not v[0].isupper():
11 raise ValueError('Le nom doit commencer par une majuscule')
12 return v
13
14 @field_validator('population')
15 @classmethod
16 def population_realistic(cls, v):
17 if v > 1_000_000:
18 raise ValueError('La population dépasse une valeur réaliste')
19 return v
20
21# La validation fonctionne
22lion = Species(name="Lion", population=120) # OK
23
24try:
25 lion = Species(name="lion", population=120)
26except ValidationError as e:
27 print(e.errors()[0]["msg"]) # Value error, Le nom doit commencer par une majusculePydantic enveloppe ta ValueError dans une ValidationError avec le préfixe « Value error ». La documentation ajoute @classmethod, car le validateur s'exécute avant que l'objet n'existe.
Une API Safari avec Pydantic
Pour finir, le modèle entre dans FastAPI. Les champs communs vont dans SpeciesBase, et des classes séparées décrivent l'entrée et la réponse :
1from fastapi import FastAPI, HTTPException
2from pydantic import BaseModel, ConfigDict, Field, field_validator
3from typing import Literal
4from datetime import datetime
5
6app = FastAPI()
7
8class SpeciesBase(BaseModel):
9 name: str = Field(..., min_length=2, max_length=100)
10 scientific_name: str = Field(..., pattern=r'^[A-Z][a-z]+ [a-z]+$')
11 population: int = Field(..., ge=0, le=1_000_000)
12 habitat: Literal["savanna", "forest", "desert", "wetland"]
13
14 @field_validator('scientific_name')
15 @classmethod
16 def validate_scientific_name(cls, v):
17 parts = v.split()
18 if len(parts) != 2:
19 raise ValueError('Le nom scientifique doit avoir le format : Genus species')
20 return v
21
22class SpeciesCreate(SpeciesBase):
23 pass
24
25class Species(SpeciesBase):
26 id: int
27 created_at: datetime = Field(default_factory=datetime.now)
28
29 model_config = ConfigDict(from_attributes=True)
30
31@app.post("/species", response_model=Species)
32async def create_species(species: SpeciesCreate):
33 # Pydantic valide automatiquement les données !
34 new_species = Species(id=1, **species.model_dump())
35 return new_speciesQuand un client envoie "habitat": "ocean", FastAPI répond avec le code 422 et create_species ne s'exécute jamais. L'ancienne class Config imbriquée a cédé la place à model_config dans Pydantic 2, et from_attributes=True servira avec la base de données.
Résumé
- BaseModel - la classe de base,
model_dump()etmodel_dump_json() - Les champs optionnels :
Optional[str] = None - La conversion automatique des types
- Les validateurs de Field (gt, ge, pattern, etc.)
- Les validateurs personnalisés (@field_validator)
- Une API Safari avec Pydantic
Prochaine leçon : les bases de données async avec SQLAlchemy !
Retiens : un modèle Pydantic est le garde à l'entrée du parc - il ne laisse passer que les données de la bonne forme.
Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. Qu'est-ce que Pydantic ?
2. De quelle classe hérite un modèle Pydantic ?
Voici 2 des 3 questions de cette leçon. Réponds aux autres dans le jeu.
Tâches pratiques dans le jeu
- Tri vertical
Ordonne la définition d'un modèle Pydantic pour une espèce du Safari :
- Tri horizontal
Comment définit-on un champ optionnel dans Pydantic ?
- Clique dans l'ordre
Ordonne un champ avec un validateur Field :
- Éditeur de code
Écris un modèle Species avec les champs : name (str), population (int > 0), habitat (str), endangered (bool, par défaut False).