Skip to main content

Pydantic: Validation de données robuste pour Python

· 7 min read

Dans un monde où les APIs et les microservices se multiplient, la validation des données est devenue une préoccupation majeure. Pydantic s'impose comme la solution de référence en Python pour définir et valider des structures de données. Découvrons ensemble cette bibliothèque puissante qui révolutionne la façon dont nous manipulons les données. 🔍

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 faciles
  • Performances optimisées grâce à l'utilisation de code compilé en Rust

Pydantic est notamment le système de modèles utilisé par FastAPI, ce qui en fait un incontournable pour les développeurs d'APIs modernes.

Installation de Pydantic 🚀

L'installation de Pydantic est simple avec pip :

pip install pydantic

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

Si vous utilisez Poetry (comme nous l'avons vu dans notre article précédent) :

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, # Sera converti en string automatiquement
email="invalid_email" # Pas d'erreur par défaut: ce n'est pas une validation de format
)
except ValueError as e:
print(f"Erreur de validation: {e}")

Sérialisation et désérialisation

Pydantic simplifie la conversion des modèles en dictionnaires, JSON ou d'autres formats :

# 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, 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 = {
# Permettre les champs supplémentaires
"extra": "forbid",
# Valider également les attributs lors de l'assignation
"validate_assignment": True,
# Aliases pour les noms de champs JSON
"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 : le duo parfait 🤝

Pydantic est particulièrement puissant lorsqu'il est utilisé avec FastAPI :

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

app = FastAPI()

class Item(BaseModel):
name: str = Field(..., example="Smartphone")
description: Optional[str] = Field(None, example="Un téléphone dernier cri")
price: float = Field(..., gt=0, example=899.99)
tax: Optional[float] = Field(None, example=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 (10-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

# 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 # Maintenant obligatoire
def check_age(cls, v):
if v < 18:
raise ValueError('Doit être majeur')
return v

# Conversion en dict/json
data = user.model_dump()
json_data = user.model_dump_json()

Bonnes pratiques avec Pydantic 👍

  1. Utilisez des types précis: Les types comme EmailStr, HttpUrl, conint, etc. améliorent la validation

  2. Créez une hiérarchie de modèles: Utilisez l'héritage pour les structures complexes

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

    class UserIn(BaseUser):
    password: str

    class UserOut(BaseUser):
    is_active: bool
  3. Exploitez les validators 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. Utilisez FrozenModel pour l'immutabilité:

    from pydantic import BaseModel, ConfigDict

    class Config(BaseModel):
    model_config = ConfigDict(frozen=True)
    api_key: str
    debug: bool = False
  5. Ajoutez des exemples pour améliorer 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 s'est imposé comme un outil indispensable dans l'écosystème Python moderne, particulièrement pour le développement d'APIs et d'applications manipulant des données structurées. Ses points forts :

  • Validation robuste des données basée sur les types Python standard
  • API intuitive permettant de définir rapidement des modèles complexes
  • Performances impressionnantes grâce au moteur de validation en Rust
  • Intégration harmonieuse avec FastAPI et d'autres frameworks