Pydantic v2 para validación de datos

Avanzado
Python
Python
Actualizado: 05/05/2026

Diagrama: tutorial-python-pydantic

Qué problema resuelve Pydantic

flowchart LR
    External["JSON entrante<br/>HTTP, MQ, form"] --> Validate["Pydantic v2<br/>BaseModel"]
    Validate -->|datos válidos| Internal["Modelo Python<br/>tipado y limpio"]
    Validate -->|datos inválidos| Error["ValidationError<br/>detalle por campo"]
    Internal --> Domain["Lógica de negocio<br/>tipos seguros"]
    Domain --> Out["model_dump_json<br/>serializa a JSON"]
    Out --> Network["Respuesta HTTP<br/>cola, persistencia"]
    Validate -.núcleo.-> Rust["pydantic-core<br/>Rust 17x más rápido"]
    style External fill:#fff3e0,stroke:#ef6c00
    style Validate fill:#e3f2fd,stroke:#1565c0
    style Error fill:#ffebee,stroke:#c62828
    style Internal fill:#e8f5e9,stroke:#2e7d32
    style Rust fill:#fce4ec,stroke:#ad1457

Los type hints de Python describen intención, pero no validan nada en ejecución. Si una función espera int y recibe "3", Python no lo impide; simplemente fallará más adelante cuando intente hacer aritmética.

Este vacío es crítico en los bordes de una aplicación, donde los datos llegan de fuera: peticiones HTTP, ficheros JSON, formularios, colas de mensajes. Ahí se necesita validar que cada campo tenga el tipo correcto, que los valores estén en rango, que no falten campos obligatorios. Sin validación explícita, cualquier bug se propaga hacia el interior de la aplicación.

Pydantic es una librería que hace justo eso: a partir de una clase declarada con type hints, valida datos en tiempo de ejecución. Su versión 2, lanzada a finales de 2023, reescribió el núcleo en Rust y es aproximadamente 17 veces más rápida que la v1.

from pydantic import BaseModel

class Usuario(BaseModel):
    id: int
    nombre: str
    email: str
    edad: int | None = None

u = Usuario(id=1, nombre="Ana", email="ana@ejemplo.com", edad=30)

Si alguno de los campos no encaja con su tipo, Pydantic lanza ValidationError con un detalle legible de qué campos fallaron y por qué.

Pydantic es la base de FastAPI, LangChain, SQLModel y muchos otros frameworks. En 2026 es prácticamente el estándar de facto para validación de datos en Python.

Declarar modelos

Un modelo se declara heredando de BaseModel. Los atributos con type hints se convierten automáticamente en campos validados:

from pydantic import BaseModel
from datetime import datetime

class Producto(BaseModel):
    id: int
    nombre: str
    precio: float
    activo: bool = True
    fecha_creacion: datetime
    tags: list[str] = []

Pydantic:

  • Valida tipos: convierte "3.14" a 3.14 cuando la entrada es string y el campo es float, pero lanza error si la conversión es imposible.
  • Aplica defaults: si no se pasa activo, usa True.
  • Parsea fechas: convierte strings ISO 8601 en datetime automáticamente.
  • Rechaza campos extra: por defecto "campo_desconocido": 5 genera error (configurable).

Crear desde dict o JSON

Los dos métodos principales son model_validate (desde dict) y model_validate_json (desde string JSON):

datos = {
    "id": 1,
    "nombre": "Laptop",
    "precio": "1299.99",
    "fecha_creacion": "2026-04-17T10:30:00",
}
p = Producto.model_validate(datos)
# Producto(id=1, nombre="Laptop", precio=1299.99, activo=True, ...)

p2 = Producto.model_validate_json('{"id": 2, "nombre": "Mouse", "precio": 29.99, "fecha_creacion": "2026-04-17T10:30:00"}')

Nótese que precio vino como string "1299.99" y Pydantic lo convirtió a float. Esto es la coerción de tipos: Pydantic intenta convertir automáticamente cuando tiene sentido.

Serializar a dict o JSON

Los métodos inversos son model_dump (a dict) y model_dump_json (a string JSON):

p.model_dump()
# {"id": 1, "nombre": "Laptop", "precio": 1299.99, ...}

p.model_dump_json()
# '{"id":1,"nombre":"Laptop","precio":1299.99,...}'

p.model_dump(exclude={"fecha_creacion"})
# sin el campo fecha_creacion

p.model_dump(mode="json")
# dict pero con valores convertidos para JSON (datetime a string, etc.)

Con exclude, include, exclude_none y by_alias se controla qué campos salen y con qué nombre.

Field: constraints y metadatos

Para validación más fina que "es del tipo correcto", se usa Field:

from pydantic import BaseModel, Field

class Usuario(BaseModel):
    id: int = Field(gt=0, description="ID positivo, mayor que cero")
    nombre: str = Field(min_length=1, max_length=100)
    edad: int = Field(ge=0, le=120)
    email: str = Field(pattern=r"^[^@]+@[^@]+\.[^@]+$")
    tags: list[str] = Field(default_factory=list, max_length=10)

Constraints útiles:

  • Números: gt, ge, lt, le (mayor, mayor o igual, menor, menor o igual).
  • Strings: min_length, max_length, pattern (regex).
  • Listas: min_length, max_length.
  • Metadatos: description, examples, title (útiles para generar documentación automática).

Cualquier violación produce ValidationError con un mensaje explícito.

Validadores personalizados

Cuando la validación no se puede expresar con Field, se usan validadores:

from pydantic import BaseModel, field_validator

class Usuario(BaseModel):
    email: str
    password: str

    @field_validator("password")
    @classmethod
    def password_fuerte(cls, v: str) -> str:
        if len(v) < 8:
            raise ValueError("al menos 8 caracteres")
        if not any(c.isdigit() for c in v):
            raise ValueError("debe contener un dígito")
        return v

@field_validator("campo") recibe el valor, lo puede validar y transformar, y retorna el valor definitivo (o lanza ValueError).

Para validación que depende de varios campos, se usa @model_validator:

from pydantic import BaseModel, model_validator

class Rango(BaseModel):
    minimo: int
    maximo: int

    @model_validator(mode="after")
    def minimo_menor_que_maximo(self):
        if self.minimo >= self.maximo:
            raise ValueError("minimo debe ser menor que maximo")
        return self

Tipos compuestos

Los modelos pueden anidarse sin problema:

class Direccion(BaseModel):
    calle: str
    ciudad: str
    pais: str

class Cliente(BaseModel):
    id: int
    nombre: str
    direcciones: list[Direccion]
    direccion_principal: Direccion | None = None

Pydantic validará recursivamente cada Direccion dentro de direcciones. Si alguna sub-entrada falla, el error indica la ruta exacta (direcciones.2.pais: field required).

Tipos nativos soportados

Pydantic entiende la mayoría de tipos Python y los valida inteligentemente:

  • Primitivos: int, float, str, bool.
  • Colecciones: list[X], dict[K, V], set[X], tuple[...].
  • Fechas: datetime, date, time, timedelta.
  • Rutas: Path.
  • UUID: uuid.UUID.
  • URLs y emails: HttpUrl, EmailStr (requieren pydantic[email]).
  • Decimales: Decimal para precisión financiera.
  • Enums: enum.Enum y enum.StrEnum.
  • Literales: Literal["a", "b"].
  • Uniones: int | str.
  • Genéricos: Generic[T].

Y también tipos propios: con un validador, cualquier tipo puede integrarse.

Configuración del modelo

Cada modelo puede tener configuración específica con la clase interna model_config (dict) o ConfigDict:

from pydantic import BaseModel, ConfigDict

class Usuario(BaseModel):
    model_config = ConfigDict(
        extra="forbid",            # rechazar campos extra
        frozen=True,               # inmutable tras crear
        str_strip_whitespace=True, # strip automático en strings
        populate_by_name=True,     # permitir tanto el alias como el nombre real
    )
    
    id: int
    nombre: str

Opciones útiles:

  • extra: "ignore" (descarta), "allow" (los acepta como atributos extra), "forbid" (lanza error).
  • frozen: hace las instancias inmutables.
  • str_strip_whitespace: aplica .strip() a todos los strings automáticamente.
  • validate_assignment: revalida cuando se asigna a un atributo tras crear.

Computed fields: campos derivados

Para valores que se calculan a partir de otros, existe @computed_field:

from pydantic import BaseModel, computed_field

class Pedido(BaseModel):
    cantidad: int
    precio_unitario: float

    @computed_field
    @property
    def total(self) -> float:
        return self.cantidad * self.precio_unitario

p = Pedido(cantidad=3, precio_unitario=10.5)
p.total                # 31.5
p.model_dump()         # incluye total: 31.5

El campo se serializa automáticamente en model_dump aunque no forme parte del constructor.

Integración con FastAPI

La integración más visible de Pydantic es FastAPI: declaras modelos para request y response y el framework hace toda la validación, documentación OpenAPI y serialización automáticamente.

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class CrearProducto(BaseModel):
    nombre: str
    precio: float

class Producto(BaseModel):
    id: int
    nombre: str
    precio: float

@app.post("/productos", response_model=Producto)
def crear(datos: CrearProducto) -> Producto:
    ...

Si el cliente envía JSON inválido, FastAPI retorna automáticamente un 422 Unprocessable Entity con el detalle del error generado por Pydantic, sin una sola línea de código de validación explícito.

Cuándo usar Pydantic y cuándo dataclass

Ambos son útiles y coexisten en una aplicación bien diseñada:

  • Pydantic: en los bordes de la aplicación. APIs HTTP, lectura de ficheros JSON, mensajes de colas, formularios. Todo lo que llega desde fuera y necesita validación.
  • Dataclass: en el núcleo de dominio. Objetos de negocio cuya validación ya ha ocurrido al entrar.

Como regla: cuando una clase representa datos que vienen de una fuente externa o se envían a una, es Pydantic. Cuando representa conceptos de negocio que tu aplicación crea y manipula internamente, dataclass (o una clase normal) suele ser suficiente y más ligero.

Pydantic se ha vuelto tan central en el ecosistema Python moderno que dominarlo es imprescindible para cualquier desarrollo profesional serio.

Alan Sastre - Autor del tutorial

Alan Sastre

Ingeniero de Software y formador, CEO en CertiDevs

Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Python es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.

Más tutoriales de Python

Explora más contenido relacionado con Python y continúa aprendiendo con nuestros tutoriales gratuitos.

Aprendizajes de esta lección

Declarar modelos con BaseModel y validación automática a partir de type hints. Construir modelos a partir de dict y JSON con model_validate y model_validate_json. Serializar con model_dump y model_dump_json. Personalizar validación con field_validator y model_validator. Usar Field para metadatos y constraints.