Aller au contenu principal

Python : Pydantic

· 8 minutes de lecture

Les données qui entrent dans une application (requêtes HTTP, fichiers de configuration, messages) n'ont aucune garantie de forme ni de type. Pydantic, bibliothèque de référence en Python pour ce besoin, valide et convertit ces données à partir de classes annotées avec les types standard du langage, et produit des erreurs détaillées lorsqu'elles ne respectent pas le modèle.

Qu'est-ce que Pydantic ?​

Pydantic est une bibliothèque Python qui permet de valider des données et de gérer les paramètres de configuration en utilisant les annotations de type Python. Elle offre plusieurs avantages :

  • Validation forte basée sur les types Python
  • Conversion automatique des données d'entrée
  • Génération de documentation JSON Schema
  • Sérialisation et désérialisation vers des dictionnaires et du JSON
  • Performances optimisées grâce à l'utilisation de code compilé en Rust

Pydantic est notamment le système de modèles utilisé par FastAPI, qui s'appuie sur lui pour valider les requêtes et sérialiser les réponses.

Installation de Pydantic​

Pydantic s'installe avec pip :

pip install pydantic

# Pour la version 2.x avec des performances optimisées
pip install "pydantic>=2.0.0"

# Avec email-validator, requis par le type EmailStr
pip install "pydantic[email]"

Avec Poetry (voir l'article Python : Poetry) :

poetry add pydantic

Les bases de Pydantic​

Définition de modèles​

La première étape avec Pydantic consiste à définir un modèle en créant une classe qui hérite de BaseModel :

from pydantic import BaseModel
from typing import Optional, List
from datetime import date

class User(BaseModel):
id: int
name: str
email: str
birth_date: date
is_active: bool = True
tags: List[str] = []
website: Optional[str] = None

Validation automatique​

Une fois le modèle défini, Pydantic valide automatiquement les données lors de la création d'une instance :

# Validation réussie
user = User(
id=1,
name="John Doe",
email="john@example.com",
birth_date="1990-01-01", # Conversion automatique en objet date
tags=["admin", "user"]
)

# Validation échouée
try:
User(
id="not_an_integer", # Erreur : la valeur n'est pas un entier
name=123, # Erreur : en v2, un int n'est pas converti en str (sauf coerce_numbers_to_str)
email="invalid_email" # Pas d'erreur par défaut : ce n'est pas une validation de format
# birth_date absent : erreur "Field required"
)
except ValueError as e:
print(f"Erreur de validation: {e}")

Sérialisation et désérialisation​

Pydantic convertit les modèles en dictionnaires ou en JSON, et inversement :

# Conversion en dictionnaire
user_dict = user.model_dump()

# Conversion en JSON
user_json = user.model_dump_json()

# Désérialisation depuis un dictionnaire
user_data = {
"id": 2,
"name": "Jane Smith",
"email": "jane@example.com",
"birth_date": "1992-03-15"
}
new_user = User.model_validate(user_data)

# Désérialisation depuis JSON
user_from_json = User.model_validate_json('{"id": 3, "name": "Bob", "email": "bob@example.com", "birth_date": "1985-07-20"}')

Fonctionnalités avancées​

Validateurs personnalisés​

Pydantic permet de créer des validateurs personnalisés pour des vérifications plus complexes :

from pydantic import BaseModel, field_validator, EmailStr

class AdvancedUser(BaseModel):
id: int
name: str
email: EmailStr # Type spécial pour valider les emails
password: str
password_confirm: str

@field_validator('name')
@classmethod
def name_must_contain_space(cls, v):
if ' ' not in v:
raise ValueError('Le nom doit contenir un espace (prénom et nom)')
return v.title() # Convertit le nom en format titre

@field_validator('password_confirm')
@classmethod
def passwords_match(cls, v, info):
if 'password' in info.data and v != info.data['password']:
raise ValueError('Les mots de passe ne correspondent pas')
return v

Types complexes​

Pydantic prend en charge une variété de types complexes :

from pydantic import BaseModel, HttpUrl, conlist, constr
from typing import Dict, Optional, Union

class Product(BaseModel):
name: str
price: float
description: Optional[str] = None

class Order(BaseModel):
order_id: str
# Une liste avec au moins 1 élément
products: conlist(Product, min_length=1)
# Une chaîne avec contrainte de longueur
customer_id: constr(min_length=5, max_length=20)
# Union de types possibles
payment_method: Union[str, Dict[str, str]]
# URL valide
store_url: HttpUrl

Configuration des modèles​

Pydantic offre de nombreuses options de configuration pour contrôler le comportement des modèles :

class Settings(BaseModel):
model_config = {
# Refuser les champs non déclarés
"extra": "forbid",
# Valider également les attributs lors de l'assignation
"validate_assignment": True,
# Accepter le nom Python du champ en plus de son alias
"populate_by_name": True,
# Noms JSON en format camelCase
"alias_generator": lambda s: ''.join(
word.capitalize() if i else word
for i, word in enumerate(s.split('_'))
),
}

database_url: str
api_key: str
debug_mode: bool = False
max_connections: int = 100

Pydantic et FastAPI​

FastAPI utilise les modèles Pydantic pour valider les corps de requête et filtrer les réponses :

from fastapi import FastAPI, Path
from pydantic import BaseModel, Field
from typing import Optional

app = FastAPI()

class Item(BaseModel):
name: str = Field(..., examples=["Smartphone"])
description: Optional[str] = Field(None, examples=["Un téléphone dernier cri"])
price: float = Field(..., gt=0, examples=[899.99])
tax: Optional[float] = Field(None, examples=[20.0])

model_config = {
"json_schema_extra": {
"examples": [
{
"name": "Smartphone",
"description": "Un téléphone dernier cri",
"price": 899.99,
"tax": 20.0,
}
]
}
}

@app.post("/items/", response_model=Item)
async def create_item(item: Item):
return item

@app.get("/items/{item_id}", response_model=Item)
async def read_item(
item_id: int = Path(..., title="L'ID de l'item à récupérer", ge=1)
):
# Logic to retrieve item
return {"name": "Example Item", "price": 99.99, "description": "A sample item"}

Avec cette configuration, FastAPI :

  • Valide automatiquement les requêtes entrantes
  • Convertit les données en objets Python typés
  • Génère une documentation OpenAPI interactive
  • Effectue la sérialisation des réponses

Pydantic v1 vs v2 : les différences majeures​

Pydantic v2 (sorti en 2023) a introduit plusieurs changements importants :

Fonctionnalitév1v2
Moteur de validationPython purCore en Rust (jusqu'à 50x plus rapide)
API.dict(), .json().model_dump(), .model_dump_json()
Validateurs@validator, @root_validator@field_validator, @model_validator
Types génériquesSupport limitéSupport amélioré
JSON SchemaGénération basiquePlus complet et personnalisable

Exemple de migration :

# Pydantic v1
from pydantic import BaseModel, validator

class UserV1(BaseModel):
name: str
age: int

@validator('age')
def check_age(cls, v):
if v < 18:
raise ValueError('Doit être majeur')
return v

user = UserV1(name="Alice", age=30)
# Conversion en dict/json
data = user.dict()
json_data = user.json()

# Pydantic v2
from pydantic import BaseModel, field_validator

class UserV2(BaseModel):
name: str
age: int

@field_validator('age')
@classmethod # Recommandé (typage) ; ajouté implicitement s'il est omis
def check_age(cls, v):
if v < 18:
raise ValueError('Doit être majeur')
return v

user = UserV2(name="Alice", age=30)
# Conversion en dict/json
data = user.model_dump()
json_data = user.model_dump_json()

Bonnes pratiques avec Pydantic​

  1. Types précis : les types comme EmailStr, HttpUrl, conint, etc. améliorent la validation

  2. Hiérarchie de modèles : l'héritage factorise les champs communs des structures complexes

    class BaseUser(BaseModel):
    id: int
    name: str

    class UserIn(BaseUser):
    password: str

    class UserOut(BaseUser):
    is_active: bool
  3. Validateurs pour les règles métier complexes :

    @field_validator("reservation_date")
    @classmethod
    def validate_date_is_future(cls, v, info):
    if v <= datetime.now():
    raise ValueError("La réservation doit être dans le futur")
    return v
  4. Modèle immuable avec ConfigDict(frozen=True) :

    from pydantic import BaseModel, ConfigDict

    class Config(BaseModel):
    model_config = ConfigDict(frozen=True)
    api_key: str
    debug: bool = False
  5. Exemples dans le schéma JSON pour la documentation :

    class Item(BaseModel):
    name: str
    price: float

    model_config = {
    "json_schema_extra": {"examples": [{"name": "Foo", "price": 35.4}]}
    }

Cas d'utilisation concrets​

Validation de configuration​

from pydantic import BaseModel, Field, SecretStr
import yaml
from pathlib import Path

class DatabaseConfig(BaseModel):
host: str = "localhost"
port: int = 5432
user: str
password: SecretStr
name: str
ssl: bool = False

class ApiConfig(BaseModel):
endpoint: str
timeout: int = 30
retries: int = 3

class AppConfig(BaseModel):
debug: bool = False
log_level: str = "INFO"
database: DatabaseConfig
api: ApiConfig

# Charger la configuration depuis un fichier YAML
config_path = Path("config.yaml")
with open(config_path) as f:
config_dict = yaml.safe_load(f)

# Valider la configuration
try:
config = AppConfig.model_validate(config_dict)
print(f"Configuration valide: {config.model_dump(exclude={'database': {'password'}})}")
except ValueError as e:
print(f"Configuration invalide: {e}")

Traitement de données d'API​

import httpx
from pydantic import BaseModel, HttpUrl
from typing import List, Optional
from datetime import datetime

class Author(BaseModel):
name: str
url: Optional[HttpUrl] = None

class Article(BaseModel):
id: int
title: str
content: str
published: datetime
author: Author
tags: List[str] = []

async def fetch_articles():
async with httpx.AsyncClient() as client:
response = await client.get("https://api.example.com/articles")
data = response.json()

# Valider et convertir les données en objets Python
articles = [Article.model_validate(item) for item in data]

# Maintenant on peut travailler avec des objets Python typés
for article in articles:
print(f"Article: {article.title}, Publié le: {article.published.strftime('%d/%m/%Y')}")
print(f"Auteur: {article.author.name}")
print("-" * 50)

return articles

Conclusion​

Pydantic est largement utilisé dans l'écosystème Python, en particulier pour les API et les applications qui manipulent des données structurées. Ses caractéristiques principales :

  • Validation des données fondée sur les annotations de types standard
  • Modèles déclaratifs composables pour décrire des structures imbriquées
  • Performances : depuis la v2, le cœur de validation (pydantic-core) est écrit en Rust
  • Intégration avec FastAPI, SQLModel et de nombreux autres frameworks

Application / Projet lié​

Mis en pratique dans le projet2026TaskHorizonSchémas Pydantic de l'API FastAPI, séparés des modèles SQLAlchemy : validation des requêtes et sérialisation des réponses sans jamais retourner un objet ORM.FastAPIReactPostgreSQLTerraformHelmAWS EKSCloudWatchGitHub Actions