Pydantic
La bibliothèque Python de validation et parsing de données par annotations de type. Tu écris une classe avec des champs typés, Pydantic valide automatiquement tout ce qui rentre — c'est le moteur sous FastAPI et désormais un standard de fait en Python moderne.
À quoi ça sert
En Python, les annotations de type (name: str, age: int)
sont par défaut purement informatives — elles ne sont pas
vérifiées à l'exécution. Si tu reçois un dict JSON depuis une API et que
l'âge arrive en "42" (string) au lieu de 42 (int),
Python s'en moque.
Pydantic comble exactement ce trou : on définit une classe qui hérite de
BaseModel, et à chaque fois qu'on crée une instance, les
données sont validées et converties selon les types
déclarés. Si quelque chose cloche, Pydantic lève une erreur claire
listant tous les problèmes.
- Validation à l'entrée — corps de requête d'une API, fichier de config, payload Kafka, etc.
- Conversion automatique —
"42"devient42si le champ estint. - Documentation auto — la classe sert de schéma, lisible par FastAPI pour générer le OpenAPI / Swagger.
- Settings d'application —
BaseSettingslit les variables d'environnement et les fichiers.env. - Sérialisation —
.model_dump()reconvertit en dict ou JSON.
Une @dataclass Python définit une classe avec des champs
typés, mais ne valide rien à l'exécution. Pydantic fait
en plus la validation, la conversion, la sérialisation, et la gestion
d'erreurs structurée. Pour de la config interne sans entrée externe, une
dataclass suffit. Dès que les données viennent d'une source
non maîtrisée (HTTP, fichier, env), Pydantic devient
indispensable.
Un exemple d'usage
Tu reçois en JSON la fiche d'un utilisateur depuis une API externe. Tu veux être sûr que les champs sont bien là et bien typés avant de continuer :
from pydantic import BaseModel, EmailStr, Field
class User(BaseModel):
id: int
name: str
email: EmailStr
age: int = Field(ge=0, le=120)
is_active: bool = True
# Cas normal : tout passe
data = {"id": "42", "name": "Alice",
"email": "a@b.fr", "age": 30}
user = User(**data)
print(user.id) # 42 (int, pas string)
print(user.model_dump()) # dict prêt à sérialiser
# Cas invalide : Pydantic lève une erreur lisible
User(id=1, name="Bob", email="pas-un-email", age=200)
# ValidationError: 2 validation errors for User
# email: value is not a valid email address
# age: Input should be less than or equal to 120
En 6 lignes, tu as déclaré un schéma typé, validé l'entrée, converti
"42" en 42, vérifié l'email, contraint l'âge entre
0 et 120. Et tu peux ressortir un dict propre avec model_dump().
How-to : installer et utiliser Pydantic
-
Installer Pydantic
Dépendance principale (cf. UV) — pour la validation d'email il faut un extra :
bashuv add "pydantic[email]"Pour
BaseSettings(lecture de fichiers.env), il faut un paquet séparé depuis Pydantic v2 :bashuv add pydantic-settingsPydantic v1 vs v2Depuis 2023, la version 2 a réécrit le moteur en Rust (×5 plus rapide) et changé quelques API :
.dict()→.model_dump(),.parse_obj()→.model_validate(). Cette fiche couvre v2. Si tu tombes sur du vieux code, vérifie la version. -
Définir un modèle de base
On hérite de
BaseModelet on déclare les champs avec des annotations standard :pythonfrom pydantic import BaseModel class Product(BaseModel): name: str price: float in_stock: bool = True # valeur par défaut p = Product(name="Stylo", price="2.50") print(p.price) # 2.5 (converti en float) print(p.model_dump()) # {'name': 'Stylo', 'price': 2.5, ...} print(p.model_dump_json()) # '{"name":"Stylo","price":2.5,...}' -
Types riches et contraintes
Pydantic comprend tous les types Python standards (incl.
list,dict,Optional,Literal) et fournit ses propres types validés :pythonfrom datetime import datetime from typing import Literal from pydantic import BaseModel, EmailStr, HttpUrl, Field class Article(BaseModel): title: str = Field(min_length=3, max_length=100) price: float = Field(gt=0) # > 0 tags: list[str] = [] status: Literal["draft", "published"] = "draft" url: HttpUrl author_email: EmailStr created: datetime = Field(default_factory=datetime.now) -
Validateurs personnalisés
Quand un type ne suffit pas, on écrit une fonction de validation avec le décorateur
@field_validator:pythonfrom pydantic import BaseModel, field_validator class SignupForm(BaseModel): username: str password: str @field_validator("username") @classmethod def no_spaces(cls, v: str) -> str: if " " in v: raise ValueError("pas d'espaces dans le username") return v.lower() # on peut aussi normaliser -
Gérer les erreurs de validation
pythonfrom pydantic import ValidationError try: user = User(id="abc", name="Alice", email="x", age=200) except ValidationError as e: print(e.errors()) # liste structurée des erreurs print(e.json()) # même chose en JSON, prêt à loggere.errors()renvoie une liste de dicts (champ, message, type d'erreur, valeur reçue) — idéal pour rendre une réponse 422 propre dans une API. -
Settings d'application avec BaseSettings
BaseSettingslit automatiquement les variables d'environnement et les fichiers.env. C'est la façon propre de configurer une application en MLOps :pythonfrom pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): model_config = SettingsConfigDict(env_file=".env") database_url: str api_key: str debug: bool = False settings = Settings() print(settings.database_url)Avec un fichier
.envcontenantDATABASE_URL=postgresql://..., Pydantic injecte la valeur. Pratique pour passer du local au déploiement (Docker, GitHub Actions…) sans changer une ligne de code. -
Sérialiser et désérialiser
python# Depuis un dict user = User.model_validate({"id": 1, "name": "…"}) # Depuis du JSON brut user = User.model_validate_json('{"id": 1, "name": "…"}') # Vers un dict user.model_dump() user.model_dump(exclude={"password"}) # Vers du JSON (string) user.model_dump_json(indent=2)
Aide-mémoire
from pydantic import BaseModel, Field, EmailStr, HttpUrl
class M(BaseModel):
x: int = Field(ge=0)
name: str = Field(min_length=1)
tags: list[str] = []
email: EmailStr
obj = M(**data)
obj = M.model_validate(data)
obj = M.model_validate_json("{...}")
obj.model_dump()
obj.model_dump_json()
from pydantic import field_validator
@field_validator("x")
@classmethod
def check(cls, v):
if v < 0: raise ValueError("…")
return v
from pydantic_settings import BaseSettings, SettingsConfigDict
class S(BaseSettings):
model_config = SettingsConfigDict(env_file=".env")
api_key: str
Pydantic et le reste de l'écosystème
-
FastAPI — c'est ici que
Pydantic brille le plus. Tout corps de requête, paramètre, modèle de
réponse est un
BaseModel. FastAPI valide automatiquement, rend des erreurs 422 propres, et génère la doc Swagger à partir des classes. - MLflow — typer le signature d'un modèle (inputs/outputs) avec Pydantic permet de garantir que le serving reçoit les bonnes données. La validation évite des bugs silencieux en production.
- SQLAlchemy — Pydantic et SQLAlchemy coexistent : SQLAlchemy gère la base, Pydantic les DTO/schémas d'API. La lib SQLModel (du créateur de FastAPI) fusionne les deux quand on veut éviter la duplication.
-
logging /
Loguru — au lieu de logger
des dicts bruts, on logge
obj.model_dump_json(): structure garantie, recherche facile dans Loki. -
Variables d'environnement — avec
BaseSettings, plus besoin de jongler entreos.environ.get()et des valeurs par défaut éparpillées. Tout est typé et centralisé dans une classe.
Beaucoup confondent x: int = None (interdit en Pydantic v2,
car incohérent) avec x: int | None = None
(autorisé : champ optionnel). Si tu veux qu'un champ puisse manquer,
sois explicite avec | None ou Optional[int],
ET donne une valeur par défaut.
Pour aller plus loin
- Site officiel : docs.pydantic.dev
- Guide de migration v1 → v2 : docs.pydantic.dev/latest/migration
- Pydantic Settings : docs.pydantic.dev/latest/concepts/pydantic_settings
- SQLModel (Pydantic + SQLAlchemy) : sqlmodel.tiangolo.com