Pydantic v2 en 2026: validación de datos en Python con el núcleo en Rust

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 usas alias, 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:

  • @validator pasa a @field_validator, y ahora es obligatorio añadir @classmethod.
  • @root_validator pasa a @model_validator.
  • class Config: pasa a model_config = ConfigDict(...).
  • .dict() pasa a .model_dump().
  • .json() pasa a .model_dump_json().
  • .parse_obj() pasa a Model.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

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP