
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"a3.14cuando 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, usaTrue. - Parsea fechas: convierte strings ISO 8601 en
datetimeautomáticamente. - Rechaza campos extra: por defecto
"campo_desconocido": 5genera 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(requierenpydantic[email]). - Decimales:
Decimalpara precisión financiera. - Enums:
enum.Enumyenum.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
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.