Pydantic es la librería de validación de datos más usada en el ecosistema Python. Pydantic v2, lanzada en 2023, reescribió el núcleo en Rust y triplicó la velocidad respecto a v1. La API cambió en aspectos importantes: model_dump() en lugar de dict(), ConfigDict en lugar de class Config, y nuevos decoradores de validación.
BaseModel: definir y validar modelos
from pydantic import BaseModel, ValidationError
class Usuario(BaseModel):
nombre: str
email: str
edad: int
activo: bool = True
# Validación automática al instanciar
u = Usuario(nombre="Ana", email="[email protected]", edad=30)
print(u)
# nombre='Ana' email='[email protected]' edad=30 activo=True
# Pydantic convierte tipos cuando puede
u2 = Usuario(nombre="Luis", email="[email protected]", edad="25") # edad como string
print(u2.edad, type(u2.edad)) # 25
# Error si los datos no son válidos
try:
Usuario(nombre="Error", email="correo", edad="no-es-numero")
except ValidationError as e:
print(e.json(indent=2))
Field() para restricciones y metadatos
from pydantic import BaseModel, Field
class Producto(BaseModel):
nombre: str = Field(..., min_length=1, max_length=100, description="Nombre del producto")
precio: float = Field(..., gt=0, description="Precio en euros")
stock: int = Field(0, ge=0, description="Unidades disponibles")
sku: str = Field(..., pattern=r"^[A-Z]{3}-d{4}$", description="Código SKU")
etiquetas: list[str] = Field(default_factory=list)
p = Producto(nombre="Teclado", precio=49.99, sku="TEC-0001")
print(p.model_dump())
# {'nombre': 'Teclado', 'precio': 49.99, 'stock': 0, 'sku': 'TEC-0001', 'etiquetas': []}
@field_validator: validar campos individuales
from pydantic import BaseModel, field_validator
class Registro(BaseModel):
username: str
password: str
email: str
@field_validator("username")
@classmethod
def username_sin_espacios(cls, v: str) -> str:
if " " in v:
raise ValueError("El username no puede contener espacios")
return v.lower()
@field_validator("password")
@classmethod
def password_segura(cls, v: str) -> str:
if len(v) < 8:
raise ValueError("La contraseña debe tener al menos 8 caracteres")
if not any(c.isdigit() for c in v):
raise ValueError("La contraseña debe contener al menos un número")
return v
@field_validator("email")
@classmethod
def email_valido(cls, v: str) -> str:
if "@" not in v or "." not in v.split("@")[-1]:
raise ValueError("Email inválido")
return v.lower()
r = Registro(username="AnaGarcia", password="segura123", email="[email protected]")
print(r.username) # anagarcia (normalizado a minúsculas)
print(r.email) # [email protected]
@model_validator: validar el modelo completo
from pydantic import BaseModel, model_validator
class Rango(BaseModel):
minimo: float
maximo: float
@model_validator(mode="after")
def validar_rango(self) -> "Rango":
if self.minimo >= self.maximo:
raise ValueError(f"minimo ({self.minimo}) debe ser menor que maximo ({self.maximo})")
return self
class PasswordConfirm(BaseModel):
password: str
confirmar_password: str
@model_validator(mode="after")
def passwords_coinciden(self) -> "PasswordConfirm":
if self.password != self.confirmar_password:
raise ValueError("Las contraseñas no coinciden")
return self
r = Rango(minimo=0.0, maximo=100.0) # OK
# Rango(minimo=50.0, maximo=10.0) # ValidationError
ConfigDict: configurar comportamiento del modelo
from pydantic import BaseModel, ConfigDict
class Config(BaseModel):
model_config = ConfigDict(
frozen=True, # instancias inmutables
str_strip_whitespace=True, # trim automático en strings
validate_default=True, # validar también los valores por defecto
extra="forbid", # error si se pasan campos no declarados
)
host: str = "localhost"
puerto: int = 5432
cfg = Config(host=" db.prod.com ")
print(cfg.host) # "db.prod.com" (trim aplicado)
# cfg.host = "otro" # TypeError (frozen)
model_dump() y serialización
from pydantic import BaseModel
from datetime import datetime
class Articulo(BaseModel):
titulo: str
contenido: str
fecha: datetime
publicado: bool = False
a = Articulo(
titulo="Pydantic v2",
contenido="Guía completa",
fecha=datetime(2024, 3, 15, 10, 0, 0)
)
# model_dump() reemplaza a dict()
print(a.model_dump())
# {'titulo': 'Pydantic v2', 'contenido': '...', 'fecha': datetime(...), 'publicado': False}
# Serializar a JSON
print(a.model_dump_json(indent=2))
# Solo algunos campos
print(a.model_dump(include={"titulo", "publicado"}))
# Excluir campos
print(a.model_dump(exclude={"contenido"}))
# Con alias para la salida
print(a.model_dump(by_alias=True))
Discriminated unions con FastAPI
from typing import Annotated, Literal
from pydantic import BaseModel, Field
class Perro(BaseModel):
tipo: Literal["perro"]
nombre: str
raza: str
class Gato(BaseModel):
tipo: Literal["gato"]
nombre: str
interior: bool
Animal = Annotated[
Perro | Gato,
Field(discriminator="tipo")
]
class Registro(BaseModel):
mascota: Animal
r1 = Registro(mascota={"tipo": "perro", "nombre": "Rex", "raza": "Labrador"})
r2 = Registro(mascota={"tipo": "gato", "nombre": "Misi", "interior": True})
print(type(r1.mascota)) #
print(type(r2.mascota)) #
Las diferencias clave entre v1 y v2: .dict() ? .model_dump(); class Config ? model_config = ConfigDict(...); @validator ? @field_validator con @classmethod; @root_validator ? @model_validator. La migración de v1 a v2 tiene una guía oficial y la herramienta bump-pydantic automatiza gran parte de los cambios.
