
Performances Pydantic : 4 conseils pour valider efficacement de grandes quantités de données
sont si faciles à utiliser qu’il est également facile de les utiliser dans le mauvais sens, comme tenir un marteau par la tête. Il en va de même pour Pydantic, une bibliothèque de validation de données hautes performances pour Python.
Dans Pydantic v2, le moteur de validation principal est implémenté dans Rouillerce qui en fait l’une des solutions de validation de données les plus rapides de l’écosystème Python. Cependant, cet avantage en termes de performances n’est réalisé que si vous utilisez Pydantic d’une manière qui exploite réellement ce noyau hautement optimisé.
Cet article se concentre sur l’utilisation efficace de Pydantic, en particulier lors de la validation de gros volumes de données. Nous mettons en évidence quatre pièges courants qui peuvent entraîner des différences de performances d’un ordre de grandeur si rien n’est fait.
1) Préférer Annotated contraintes sur les validateurs de terrain
Une fonctionnalité essentielle de Pydantic est que la validation des données est définie de manière déclarative dans une classe de modèle. Lorsqu’un modèle est instancié, Pydantic analyse et valide les données d’entrée en fonction des types de champs et des validateurs définis sur cette classe.
L’approche naïve : les validateurs de terrain
Nous utilisons un @field_validator pour valider les données, comme vérifier si un id La colonne est en fait un entier ou supérieur à zéro. Ce style est lisible et flexible mais a un coût en termes de performances.
class UserFieldValidators(BaseModel):
id: int
email: EmailStr
tags: list[str]
@field_validator("id")
def _validate_id(cls, v: int) -> int:
if not isinstance(v, int):
raise TypeError("id must be an integer")
if v < 1:
raise ValueError("id must be >= 1")
return v
@field_validator("email")
def _validate_email(cls, v: str) -> str:
if not isinstance(v, str):
v = str(v)
if not _email_re.match(v):
raise ValueError("invalid email format")
return v
@field_validator("tags")
def _validate_tags(cls, v: list[str]) -> list[str]:
if not isinstance(v, list):
raise TypeError("tags must be a list")
if not (1 <= len(v) <= 10):
raise ValueError("tags length must be between 1 and 10")
for i, tag in enumerate(v):
if not isinstance(tag, str):
raise TypeError(f"tag[{i}] must be a string")
if tag == "":
raise ValueError(f"tag[{i}] must not be empty")
La raison en est que les validateurs de champ s’exécutent dans Python, après coercition de type de base et validation des contraintes. Cela les empêche d’être optimisés ou fusionnés dans le pipeline de validation principal.
L’approche optimisée : Annotated
Nous pouvons utiliser Annotated de Python typing bibliothèque.
class UserAnnotated(BaseModel):
id: Annotated[int, Field(ge=1)]
email: Annotated[str, Field(pattern=RE_EMAIL_PATTERN)]
tags: Annotated[list[str], Field(min_length=1, max_length=10)]
Cette version est plus courte, plus claire et montre une exécution plus rapide à grande échelle.
Pourquoi Annotated est plus rapide
Annotated (PEP 593) est une fonctionnalité Python standard, du typing bibliothèque. Les contraintes placées à l’intérieur Annotated sont compilés dans le schéma interne de Pydantic et exécutés dans pydantic-core (Rust).
Cela signifie qu’aucun appel de validation Python défini par l’utilisateur n’est requis pendant la validation. De plus, aucun objet Python intermédiaire ni flux de contrôle personnalisé n’est introduit.
En revanche, @field_validator fonctions toujours exécuté en Python, introduisez une surcharge d’appel de fonction et dupliquez souvent des vérifications qui auraient pu être gérées lors de la validation principale.
Nuance importante
Une nuance importante est que Annotated lui-même n’est pas « Rust ». L’accélération vient de l’utilisation de contraintes que pydantic-core comprend et peut utiliser, et non de Annotated existant par lui-même.
Référence
La différence entre aucune validation et <strong>Annotated</strong> validation est négligeable dans ces benchmarks, tandis que les validateurs Python peuvent devenir une différence d’un ordre de grandeur.

Benchmark (time in seconds)
┏━━━━━━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━━┓
┃ Method ┃ n=100 ┃ n=1k ┃ n=10k ┃ n=50k ┃
┡━━━━━━━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━━┩
│ FieldValidators│ 0.004 │ 0.020 │ 0.194 │ 0.971 │
│ No Validation │ 0.000 │ 0.001 │ 0.007 │ 0.032 │
│ Annotated │ 0.000 │ 0.001 │ 0.007 │ 0.036 │
└────────────────┴───────────┴──────────┴───────────┴───────────┘
En valeur absolue on passe de près d’une seconde de temps de validation à 36 millisecondes. Une augmentation des performances de près de 30x.
Verdict
Utiliser Annotated chaque fois que possible. Vous obtenez meilleures performances et des modèles plus clairs. Les validateurs personnalisés sont puissants, mais vous payez pour cette flexibilité en termes de coût d’exécution, alors réservez @field_validator pour une logique qui ne peut pas être exprimée sous forme de contraintes.
2). Validez JSON avec model_validate_json()
Nous avons des données sous la forme d’une chaîne JSON. Quelle est la manière la plus efficace de valider ces données ?
L’approche naïve
Analysez simplement le JSON et validez le dictionnaire :
py_dict = json.loads(j)
UserAnnotated.model_validate(py_dict)
L’approche optimisée
Utilisez une fonction Pydantic :
UserAnnotated.model_validate_json(j)
Pourquoi c’est plus rapide
model_validate_json()analyse JSON et le valide dans un seul pipeline- Il utilise Pydantic interne et un analyseur JSON plus rapide
- Cela évite de créer de grands dictionnaires Python intermédiaires et de parcourir ces dictionnaires une seconde fois lors de la validation.
Avec json.loads() vous payez deux fois : d’abord lors de l’analyse de JSON en objets Python, puis pour valider et contraindre ces objets.
model_validate_json() réduit les allocations de mémoire et les parcours redondants.
Évalué
La version Pydantic est presque deux fois plus rapide.

Benchmark (time in seconds)
┏━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━┳━━━━━━━┳━━━━━━━┳━━━━━━━┳━━━━━━━━┓
┃ Method ┃ n=100 ┃ n=1K ┃ n=10K ┃ n=50K ┃ n=250K ┃
┡━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━╇━━━━━━━╇━━━━━━━╇━━━━━━━╇━━━━━━━━┩
│ Load json │ 0.000 │ 0.002 │ 0.016 │ 0.074 │ 0.368 │
│ model validate json │ 0.001 │ 0.001 │ 0.009 │ 0.042 │ 0.209 │
└─────────────────────┴───────┴───────┴───────┴───────┴────────┘
En termes absolus, le changement nous fait gagner 0,1 seconde en validant un quart de million d’objets.
Verdict
Si votre entrée est JSON, laissez Pydantic gérer l’analyse et la validation en une seule étape. En termes de performances, il n’est pas absolument nécessaire de l’utiliser model_validate_json() mais faites-le quand même pour éviter de créer des objets Python intermédiaires et condenser votre code.
3) Utiliser TypeAdapter pour une validation groupée
Nous avons un User modèle et maintenant nous voulons valider un list de Users.
L’approche naïve
Nous pouvons parcourir la liste et valider chaque entrée ou créer un modèle wrapper. Supposer batch est un list[dict]:
# 1. Per-item validation
models = [User.model_validate(item) for item in batch]
# 2. Wrapper model
# 2.1 Define a wrapper model:
class UserList(BaseModel):
users: list[User]
# 2.2 Validate with the wrapper model
models = UserList.model_validate({"users": batch}).users
Approche optimisée
Les adaptateurs de type sont plus rapides pour valider des listes d’objets.
ta_annotated = TypeAdapter(list[UserAnnotated])
models = ta_annotated.validate_python(batch)
Pourquoi c’est plus rapide
Laissez le gros du travail à Rust. L’utilisation d’un TypeAdapter ne nécessite pas la construction d’un Wrapper supplémentaire et la validation s’exécute à l’aide d’un schéma compilé unique. Il y a moins de franchissements de limites Python-Rust-and-back et la surcharge d’allocation d’objets est inférieure.
Les modèles Wrapper sont plus lents car ils font plus que valider la liste :
- Construit une instance de modèle supplémentaire
- Suit les ensembles de champs et l’état interne
- Gère la configuration, les valeurs par défaut, les extras
Cette couche supplémentaire est petite par appel, mais devient mesurable à grande échelle.
Évalué
Lorsque nous utilisons de grands ensembles, nous constatons que l’adaptateur de type est nettement plus rapide, notamment par rapport au modèle wrapper.

Benchmark (time in seconds)
┏━━━━━━━━━━━━━━┳━━━━━━━┳━━━━━━━┳━━━━━━━┳━━━━━━━┳━━━━━━━━┳━━━━━━━━┓
┃ Method ┃ n=100 ┃ n=1K ┃ n=10K ┃ n=50K ┃ n=100K ┃ n=250K ┃
┡━━━━━━━━━━━━━━╇━━━━━━━╇━━━━━━━╇━━━━━━━╇━━━━━━━╇━━━━━━━━╇━━━━━━━━┩
│ Per-item │ 0.000 │ 0.001 │ 0.021 │ 0.091 │ 0.236 │ 0.502 │
│ Wrapper model│ 0.000 │ 0.001 │ 0.008 │ 0.108 │ 0.208 │ 0.602 │
│ TypeAdapter │ 0.000 │ 0.001 │ 0.021 │ 0.083 │ 0.152 │ 0.381 │
└──────────────┴───────┴───────┴───────┴───────┴────────┴────────┘
En termes absolus, cependant, l’accélération nous fait gagner environ 120 à 220 millisecondes pour 250 000 objets.
Verdict
Lorsque vous souhaitez simplement valider un type, pas définir un objet de domaine, TypeAdapter est l’option la plus rapide et la plus propre. Bien que cela ne soit pas absolument nécessaire pour gagner du temps, il évite l’instanciation inutile du modèle et évite les boucles de validation côté Python, rendant votre code plus propre et plus lisible.
4) Évitez from_attributes sauf si tu en as besoin
Avec from_attributes vous configurez votre classe de modèle. Lorsque vous le réglez sur True vous dites à Pydantic de lire les valeurs des attributs d’objet au lieu des clés du dictionnaire. Cela est important lorsque votre entrée est autre chose qu’un dictionnaire, comme une instance SQLAlchemy ORM, une classe de données ou tout objet Python simple avec des attributs.
Par défaut from_attributes est False. Parfois, les développeurs définissent cet attribut sur True pour garder le modèle flexible :
class Product(BaseModel):
id: int
name: str
model_config = ConfigDict(from_attributes=True)
Toutefois, si vous transmettez simplement des dictionnaires à votre modèle, il est préférable d’éviter from_attributes car cela nécessite que Python fasse beaucoup plus de travail. La surcharge qui en résulte n’offre aucun avantage lorsque l’entrée est déjà en mappage simple.
Pourquoi from_attributes=True est plus lent
Cette méthode utilise getattr() au lieu de la recherche dans le dictionnaire, qui est plus lente. Cela peut également déclencher des fonctionnalités sur l’objet à partir duquel nous lisons, comme des descripteurs, des propriétés ou un chargement paresseux ORM.
Référence
À mesure que la taille des lots augmente, l’utilisation des attributs devient de plus en plus coûteuse.

Benchmark (time in seconds)
┏━━━━━━━━━━━━━━┳━━━━━━━┳━━━━━━━┳━━━━━━━┳━━━━━━━┳━━━━━━━━┳━━━━━━━━┓
┃ Method ┃ n=100 ┃ n=1K ┃ n=10K ┃ n=50K ┃ n=100K ┃ n=250K ┃
┡━━━━━━━━━━━━━━╇━━━━━━━╇━━━━━━━╇━━━━━━━╇━━━━━━━╇━━━━━━━━╇━━━━━━━━┩
│ with attribs │ 0.000 │ 0.001 │ 0.011 │ 0.110 │ 0.243 │ 0.593 │
│ no attribs │ 0.000 │ 0.001 │ 0.012 │ 0.103 │ 0.196 │ 0.459 │
└──────────────┴───────┴───────┴───────┴───────┴────────┴────────┘
En termes absolus, un peu moins de 0,1 seconde est économisée lors de la validation de 250 000 objets.
Verdict
Utiliser uniquement from_attributes lorsque votre entrée est pas un dicton. Il existe pour prendre en charge les objets basés sur des attributs (ORM, classes de données, objets de domaine). Dans ces cas, cela peut être plus rapide que de vider d’abord l’objet dans un dict, puis de le valider. Pour les mappages simples, cela ajoute une surcharge sans aucun avantage.
Conclusion
Le but de ces optimisations n’est pas de gagner quelques millisecondes pour le plaisir. En termes absolus, même une différence de 100 ms constitue rarement un goulot d’étranglement dans un système réel.
La vraie valeur réside dans l’écriture d’un code plus clair et dans la bonne utilisation de vos outils.
L’utilisation des conseils spécifiés dans cet article conduit à des modèles plus clairsplus intention expliciteet un meilleur alignement avec la façon dont Pydantic est conçu pour fonctionner. Ces modèles déplacent la logique de validation du code Python ad hoc vers des schémas déclaratifs qui sont plus facile à lire, à raisonner et à entretenir.
Les améliorations de performances sont un effet secondaire de l’action la bonne manière. Lorsque les règles de validation sont exprimées de manière déclarative, Pydantic peut les appliquer de manière cohérente, les optimiser en interne et les faire évoluer naturellement à mesure que vos données augmentent.
En bref:
N’adoptez pas ces modèles simplement parce qu’ils sont plus rapides. Adoptez-les car ils rendent votre code plus simple, plus explicite et mieux adapté aux outils que vous utilisez.
L’accélération n’est qu’un joli bonus.
J’espère que cet article était aussi clair que je le souhaitais, mais si ce n’est pas le cas, faites-moi savoir ce que je peux faire pour clarifier davantage. En attendant, consultez mon d’autres articles sur toutes sortes de sujets liés à la programmation.
Bon codage !
-Mike
Ps : tu aimes ce que je fais ? Suis-moi!



