Pydantic es una librerÃa de Python que valida y transforma datos usando type hints como contrato. No tienes que escribir lógica de validación manual: declaras los tipos, y Pydantic se encarga del resto cuando construyes el objeto.
Lo usan para muchas cosas: validar los datos que llegan en una petición HTTP (FastAPI lo usa internamente para eso), parsear archivos de configuración, transformar filas de base de datos en objetos tipados, o verificar que las respuestas de un LLM tienen el formato que esperas. Si trabajas con aplicaciones RAG, ya te habrás encontrado con Pydantic para estructurar esas respuestas.
En junio de 2023 salió la versión 2. La novedad principal fue reescribir el núcleo de validación en Rust, lo que supone entre 5 y 50 veces más velocidad que v1 según los benchmarks oficiales. La API también cambió bastante: varios decoradores y métodos tienen nombres nuevos, y la configuración del modelo ya no va en una clase interna Config.
La anatomÃa de un modelo
Todo empieza con BaseModel. Defines una clase que hereda de ella, declaras los campos con sus tipos, y listo.
from pydantic import BaseModel
class Usuario(BaseModel):
nombre: str
email: str
edad: int = 18
Cuando construyes el objeto, Pydantic valida los datos en ese momento:
u = Usuario(nombre="Ana", email="[email protected]")
# edad toma el valor por defecto: 18
u2 = Usuario(nombre=123, email="[email protected]")
# Pydantic convierte 123 a "123" — esto se llama coerción
Para sacar los datos como diccionario usas model_dump(), que es el nombre nuevo del antiguo .dict():
u.model_dump()
# {"nombre": "Ana", "email": "[email protected]", "edad": 18}
Si necesitas JSON directamente, tienes model_dump_json(). Y para construir el modelo desde un diccionario externo, Model.model_validate(dict) sustituye al viejo .parse_obj().
Field(): validación declarativa
Con Field() añades restricciones sobre el valor de cada campo sin escribir lógica extra:
from pydantic import BaseModel, Field
class Producto(BaseModel):
nombre: str = Field(min_length=2, max_length=50)
precio: float = Field(gt=0)
email_contacto: str = Field(pattern=r'^[w.-]+@[w.-]+.w+$')
stock: int = Field(ge=0, le=9999)
Los parámetros más usados son gt (mayor que), ge (mayor o igual), lt (menor que), le (menor o igual), min_length y max_length. El parámetro pattern acepta una expresión regular.
También puedes usar alias si el JSON de entrada tiene un nombre de campo distinto al que quieres en tu modelo:
class Perfil(BaseModel):
nombre_usuario: str = Field(alias="user_name")
Asà puedes recibir {"user_name": "ana"} y trabajar internamente con perfil.nombre_usuario.
@field_validator: lógica de validación personalizada
Cuando las restricciones de Field() no son suficientes, usas @field_validator. Es el nombre que Pydantic v2 le da al antiguo @validator.
from pydantic import BaseModel, field_validator
class Usuario(BaseModel):
email: str
nombre: str
@field_validator('email')
@classmethod
def validar_email(cls, v):
if '@' not in v:
raise ValueError('El email no tiene formato válido')
return v.lower() # normalizamos a minúsculas
FÃjate en el @classmethod: es obligatorio en v2. Si no lo pones, Pydantic lanza un warning.
Puedes controlar cuándo se ejecuta el validador con el parámetro mode:
mode='before': se ejecuta antes de que Pydantic convierta el tipo. Recibes el valor raw tal como llegó.mode='after'(por defecto): se ejecuta sobre el valor ya convertido al tipo declarado.
@field_validator('tags', mode='before')
@classmethod
def normalizar_tags(cls, v):
# v puede ser una cadena "python,pydantic" o ya una lista
if isinstance(v, str):
return [t.strip() for t in v.split(',')]
return v
@model_validator: validaciones cruzadas entre campos
Cuando la validación depende de más de un campo a la vez, @field_validator no llega. Para eso existe @model_validator, que sustituye al antiguo @root_validator.
from pydantic import BaseModel, model_validator
from datetime import date
class Evento(BaseModel):
fecha_inicio: date
fecha_fin: date
titulo: str
@model_validator(mode='after')
def validar_fechas(self):
if self.fecha_fin < self.fecha_inicio:
raise ValueError('La fecha de fin no puede ser anterior a la de inicio')
return self
Con mode='after' recibes el modelo ya construido (self), con todos los campos convertidos y validados individualmente. Con mode='before' recibes el diccionario raw antes de que Pydantic haga nada, lo que viene bien si necesitas transformar la estructura de entrada antes de validarla.
model_config = ConfigDict(...): configuración del modelo
En v1 la configuración iba en una clase interna llamada Config. En v2 eso desaparece y se usa ConfigDict:
from pydantic import BaseModel, ConfigDict
class Articulo(BaseModel):
model_config = ConfigDict(
str_strip_whitespace=True, # quita espacios al principio y al final de strings
frozen=True, # el modelo es inmutable una vez creado
validate_default=True, # valida también los valores por defecto
populate_by_name=True, # acepta tanto el alias como el nombre del campo
)
titulo: str
slug: str
Algunas opciones que más se usan:
str_strip_whitespace=True: evita que un string con espacios de más pase la validación sin querer.frozen=True: hace el modelo inmutable. Intentar cambiar un campo lanza un error. Útil si vas a usar el modelo como clave de diccionario o en sets.validate_default=True: por defecto Pydantic no valida los valores por defecto. Con esto activado, sÃ.populate_by_name=True: si usasalias, permite que el campo funcione tanto con el alias como con el nombre original.
Tipos especiales: EmailStr, HttpUrl, UUID y anotaciones
Pydantic incluye varios tipos listos para usar que van más allá de los tipos básicos de Python:
from pydantic import BaseModel, EmailStr, HttpUrl
from uuid import UUID
class Cuenta(BaseModel):
id: UUID
email: EmailStr # valida que sea un email bien formado
web: HttpUrl # valida y parsea la URL
EmailStr requiere instalar pydantic[email] o el paquete email-validator por separado. HttpUrl viene incluida y también parsea el scheme, host y path.
Para añadir restricciones a tipos básicos sin crear un campo Field(), puedes usar Annotated junto con StringConstraints u otros:
from typing import Annotated
from pydantic import StringConstraints
SlugStr = Annotated[str, StringConstraints(
min_length=3,
max_length=80,
pattern=r'^[a-z0-9-]+$'
)]
class Post(BaseModel):
slug: SlugStr
Esto es especialmente útil cuando el mismo tipo con las mismas restricciones aparece en varios modelos: lo defines una vez como alias de tipo y lo reutilizas.
BaseSettings: configuración desde variables de entorno
Pydantic tiene un paquete adicional, pydantic-settings, que extiende BaseModel para leer configuración desde variables de entorno o archivos .env:
from pydantic_settings import BaseSettings, SettingsConfigDict
class AppConfig(BaseSettings):
model_config = SettingsConfigDict(
env_file='.env',
env_file_encoding='utf-8',
)
db_url: str
secret_key: str
debug: bool = False
max_connections: int = 10
Con esto, si tienes un archivo .env con DB_URL=postgresql://..., Pydantic lo lee automáticamente y valida que sea un string. Si la variable no existe y no tiene valor por defecto, lanza un error al arrancar la aplicación, no cuando intentes usarla por primera vez. Eso te ahorra sorpresas en producción.
En FastAPI lo habitual es instanciar la configuración una sola vez con @lru_cache para no releer el archivo en cada petición:
from functools import lru_cache
@lru_cache
def get_config():
return AppConfig()
Si te interesan los agentes de producción con APIs multi-modelo, BaseSettings es la forma limpia de gestionar las API keys de cada proveedor sin hardcodearlas en el código.
Resumen de cambios de v1 a v2
Si tienes código con Pydantic v1, esto es lo que cambia:
@validatorpasa a@field_validator, y ahora es obligatorio añadir@classmethod.@root_validatorpasa a@model_validator.class Config:pasa amodel_config = ConfigDict(...)..dict()pasa a.model_dump()..json()pasa a.model_dump_json()..parse_obj()pasa aModel.model_validate().
Los métodos viejos siguen funcionando en v2 pero emiten deprecation warnings. Si ves esos avisos en los logs, ya sabes por dónde empezar la migración.
Imagen: Pexels / alleksana
