El Modelo de Límite de Pydantic
Cada servicio Python eventualmente tiene que responder la misma pregunta: ¿qué haces con los datos que llegan fuera del control de tu programa? Un cuerpo de solicitud HTTP, un archivo de configuración, un mensaje de una cola, una fila de un CSV que alguien subió.
La respuesta de Pydantic es convertir esos datos entrantes en un objeto Python fuertemente tipado exactamente una vez, en el punto en que cruzan hacia tu código, y rechazar o coaccionar cualquier cosa que no se ajuste a la forma que declaraste.
Conceptos Básicos de Pydantic muestra la sintaxis para definir esa forma con BaseModel; esta página trata sobre el modelo subyacente: por qué la validación ocurre donde ocurre, cómo pydantic-core la hace lo suficientemente rápida como para usarla en todas partes, y por qué volcar datos de salida es una operación genuinamente diferente de describir su forma.
Resumen
- Pydantic analiza la entrada no confiable y débilmente tipada en un objeto Python fuertemente tipado en un único cruce de límite, y luego trata ese objeto como confiable durante el resto de su vida útil.
- Por Qué Importa: Sin una capa de límite, las comprobaciones de corrección de tipos se dispersan en cada función que toca datos externos, y "¿es esto un
Userválido?" se convierte en una pregunta que cada llamador tiene que responder de nuevo en lugar de una garantía que el sistema de tipos ya te da. - Conceptos Clave: validación de límite, pydantic-core, esquema central, coerción, modo estricto, serialización.
- Cuándo Usar: Analizar cuerpos de solicitud, archivos de configuración y variables de entorno; validar filas que provienen de un CSV o un mensaje de cola; construir objetos de dominio tipados en los que el código posterior pueda confiar sin volver a verificar.
- Limitaciones / Compensaciones: Pydantic valida la forma y el tipo, no las reglas de negocio que dependen de otras filas o del estado externo; la validación se ejecuta en la construcción, por lo que mutar un modelo después de la creación omite esas comprobaciones a menos que opte explícitamente por participar nuevamente.
- Temas Relacionados: dataclasses y attrs, generación de JSON Schema, validadores a nivel de campo, gestión de configuraciones.
Fundamentos
La frase "analizar, no validar" describe el cambio mental que Pydantic te pide que hagas.
Una función de validación que devuelve True o False te dice que los datos fueron aceptables en un momento dado, pero te devuelve el mismo dict o str sin tipar con el que empezaste, por lo que cada función posterior todavía tiene que confiar en que esa comprobación se realizó correctamente aguas arriba.
Un analizador, en cambio, convierte esos datos en un tipo nuevo y más específico: un objeto User en lugar de un dict. Y una vez que ese objeto existe, su existencia es la prueba de que la validación ya tuvo éxito.
BaseModel es el analizador de Pydantic: declaras la forma de destino con anotaciones de tipo de Python ordinarias, y la instanciación de la clase con datos crudos produce una instancia completamente tipada o genera una ValidationError que te dice exactamente qué no encajó.
Una analogía útil es un puesto fronterizo que también cambia divisas.
Los datos no solo se inspeccionan y se dejan pasar en su forma original; se convierten a la moneda local (tu modelo tipado) en el punto de cruce, por lo que nada aguas abajo tiene que seguir preguntando qué moneda está sosteniendo.
Pydantic 2 reconstruyó este puesto fronterizo sobre pydantic-core, un motor de validación escrito en Rust, que es por lo que el mismo estilo declarativo que ya era conveniente en Pydantic 1 se volvió lo suficientemente rápido como para usarlo en cada solicitud en una API de alto tráfico en lugar de reservarlo para envíos de formularios y carga de configuración.
Mecánica e Interacciones
La historia del rendimiento comienza en la definición de la clase, no en la instanciación.
Cuando se define una subclase de BaseModel, Pydantic recorre sus anotaciones de tipo y configuración de campo y las compila una vez en un esquema central —una estructura de datos que pydantic-core entiende— en lugar de reinterpretar tus anotaciones de tipo desde cero cada vez que se crea una instancia.
Cada llamada posterior a User(**data) reutiliza ese esquema ya compilado y realiza la comprobación real campo por campo dentro de la extensión de Rust, que es por lo que validar mil registros con Pydantic 2 cuesta aproximadamente una compilación de esquema más mil validaciones nativas baratas, no mil inspecciones de tipos dinámicas costosas.
class User(BaseModel):
id: int
email: str
# El esquema para User se compila una vez, aquí, en la definición de la clase:
User.__pydantic_core_schema__ # la forma compilada contra la que valida pydantic-core
# Cada instanciación a continuación reutiliza ese esquema; no se recompila por llamada:
User(id="1", email="a@example.com") # "1" se coacciona a int en modo laxo (predeterminado)Ese fragmento también muestra la coerción, que es el comportamiento predeterminado y "lax" (laxo): se acepta y convierte una cadena "1" para un campo int, porque en la práctica muchos datos legítimos (campos de formulario, cadenas de consulta, números JSON que llegaron como texto) son técnicamente del tipo incorrecto pero inequívocamente convertibles.
El modo estricto desactiva eso, rechazando cualquier cosa que no sea ya el tipo declarado, lo que importa para los límites donde la coerción silenciosa ocultaría un error real: una llamada interna de servicio a servicio donde un id de tipo str en lugar de int generalmente indica un error del llamador, no una peculiaridad de formato.
Los validadores (field_validator, model_validator) superponen lógica personalizada al esquema compilado en lugar de reemplazarlo: un field_validator se ejecuta después de que pydantic-core ya ha coaccionado y verificado el tipo de ese campo, por lo que opera sobre datos que ya puede confiar que son del tipo Python correcto, no sobre la entrada cruda.
Consideraciones Avanzadas y Aplicaciones
model_dump(), model_dump_json() y model_json_schema() suenan como tres sabores de la misma característica, pero responden a tres preguntas no relacionadas, y confundirlas es uno de los errores arquitectónicos más comunes en las bases de código con mucho Pydantic.
model_dump() y model_dump_json() son serialización: toman una instancia real con datos reales y producen un dict de Python o una cadena JSON a partir de ella, y Pydantic ejecuta esto a través de su propio esquema de serialización compilado, que es por lo que la lógica personalizada de field_serializer y opciones como exclude_none o by_alias solo afectan a estas llamadas.
model_json_schema() nunca mira una instancia; inspecciona la definición de la clase y produce un documento JSON Schema que describe cómo se vería una instancia válida, que es la forma que FastAPI publica como OpenAPI y la forma que consume una herramienta de generación de código del cliente; es una descripción estática, no una transformación de datos.
La consecuencia práctica es que cambiar un field_serializer cambia lo que produce model_dump_json() pero nunca cambia model_json_schema(), y viceversa: una Field(description=...) a nivel de esquema aparece en el JSON Schema pero no tiene ningún efecto en un volcado.
Dónde debe vivir la validación es una decisión arquitectónica relacionada y más importante: empujar cada comprobación al modelo en el límite HTTP no es lo mismo que empujar cada comprobación solo allí.
| Ubicación de la validación | Fortaleza | Debilidad | Mejor Ajuste |
|---|---|---|---|
| Límite del framework (modelos de solicitud de FastAPI) | Rechaza la entrada mal formada antes de que se ejecute cualquier código de manejador; documentación OpenAPI gratuita | Solo ve una solicitud de forma aislada; no puede verificar reglas que abarcan múltiples recursos | Forma, tipo y reglas de negocio de un solo campo en cada solicitud entrante |
Validadores a nivel de modelo (model_validator) | Las reglas entre campos permanecen junto a los campos que restringen | Todavía limitado a lo que hay dentro de esa única instancia del modelo | Reglas como "la fecha de finalización debe ser posterior a la fecha de inicio" |
| Comprobaciones de la capa de servicio | Puede consultar otros datos (unicidad, propiedad, cuotas) | Fácil de omitir si un método de servicio se llama desde múltiples puntos de entrada | Reglas que necesitan una consulta a la base de datos o el estado de otro servicio |
| Restricciones de la base de datos | Se aplican incluso si el código de la aplicación tiene un error | Los errores aparecen como excepciones genéricas de la base de datos, no como ValidationErrors amigables | Último respaldo para unicidad, claves foráneas y garantías de no nulo |
Ninguna fila reemplaza a las otras: una restricción de correo electrónico único en la base de datos es el respaldo para una comprobación que ya debería haberse realizado, más rápido y con un mejor mensaje de error, en la capa del modelo o del servicio.
El papel de Pydantic se reduce a medida que desciendes en esa tabla: posee la forma y el tipo en el borde, y no es la capa responsable de "¿este correo electrónico ya existe?" o "¿tiene este usuario permiso para actualizar este registro?".
Conceptos Erróneos Comunes
- "La validación de Pydantic significa que mis datos son correctos según las reglas de negocio." Pydantic confirma que un campo es un
intdentro de las restricciones declaradas; no tiene idea de si ese entero es un saldo de cuenta válido para este usuario específico, lo que es una preocupación de la capa de servicio o de la capa de base de datos. - "
model_dump()y el antiguo.dict()hacen lo mismo.".dict()es un shim de compatibilidad de Pydantic 1;model_dump()es la ruta nativa de v2 que respeta los serializadores personalizados y la distinciónmode="json", y mezclar los dos en una base de código produce resultados sutilmente inconsistentes. - "
model_json_schema()refleja el comportamiento de serialización en tiempo de ejecución." Refleja solo la forma declarada de la clase; unfield_serializerque cambia cómo se ve un volcado no tiene ningún efecto en el JSON Schema generado, porque la generación de esquemas nunca ejecuta una instancia a través de la serialización. - "Una vez que se construye un modelo, sus campos permanecen válidos." La validación se ejecuta en el momento de
__init__; asignar un nuevo valor al atributo de una instancia existente omite completamente esa comprobación a menos que se establezcamodel_config = ConfigDict(validate_assignment=True). - "El modo estricto es lo que deberías usar en todas partes por seguridad." El modo estricto rechaza datos legítimos que llegan comúnmente, como cadenas numéricas de parámetros de consulta; la coerción laxa (la predeterminada) es lo que hace que Pydantic sea utilizable en los límites HTTP en primer lugar, y el modo estricto es para límites internos más estrechos y ya tipados.
Preguntas Frecuentes
¿Qué significa realmente "analizar, no validar" en el contexto de Pydantic?
Significa que Pydantic no solo devuelve True/False sobre si los datos son aceptables, sino que convierte esos datos en un nuevo objeto tipado.
Una vez que existe una instancia de User, su existencia es la prueba de que la validación ya pasó, por lo que los llamadores no necesitan volver a verificarla.
¿Por qué Pydantic 2 es mucho más rápido que Pydantic 1?
Pydantic 2 trasladó el trabajo real de validación a pydantic-core, una extensión de Rust, en lugar de hacerlo en Python puro. La API orientada a Python (BaseModel, anotaciones de tipo) se mantuvo familiar, pero el esquema se compila una vez y se ejecuta de forma nativa en cada instanciación.
¿Qué es un "esquema central" y cuándo se construye?
Es la representación interna compilada de la forma de un modelo contra la cual pydantic-core realmente valida. Se construye una vez, cuando se define la subclase BaseModel, no se deriva de tus anotaciones de tipo en cada llamada.
¿Cuál es la diferencia entre el modo laxo y el modo estricto?
- El modo laxo (el predeterminado) coacciona tipos compatibles, como una cadena numérica a un
int. - El modo estricto rechaza cualquier cosa que no sea ya el tipo exacto declarado.
- El modo laxo se ajusta a límites débilmente tipados como las cadenas de consulta HTTP; el modo estricto se ajusta a llamadas internas de servicio ya tipadas.
¿Cómo se diferencia `model_dump()` de `model_json_schema()`?
model_dump() toma los datos de una instancia real y los serializa a un dict o JSON. model_json_schema() nunca toca una instancia: inspecciona la definición de la clase y produce un JSON Schema que describe la forma válida. Cambiar un serializador afecta al primero, nunca al segundo.
¿Se ejecutan los validadores de campo antes o después de la coerción de tipo?
Después. Un field_validator recibe datos que pydantic-core ya ha coaccionado y confirmado que coinciden con el tipo declarado, por lo que el código del validador personalizado puede asumir que está trabajando con el tipo Python correcto ya.
¿Pydantic sigue validando un modelo después de que se ha creado?
No, por defecto. La validación ocurre en la construcción; mutar un atributo después omite esa comprobación por completo a menos que el modelo establezca model_config = ConfigDict(validate_assignment=True).
¿Dónde deben vivir las reglas de negocio que necesitan una consulta a la base de datos?
No dentro del modelo Pydantic, ya que no tiene acceso a una sesión de base de datos. Esas pertenecen a una capa de servicio que puede consultar cosas como unicidad o propiedad, con la base de datos misma como una restricción de respaldo final.
¿Es Pydantic útil solo para cuerpos de solicitud de FastAPI?
No, se usa con la misma frecuencia para cargar configuraciones/ajustes, analizar mensajes de cola, validar filas de un archivo y definir objetos de dominio tipados que se pasan entre servicios internos, independientemente de cualquier framework web.
¿Por qué Pydantic rechaza campos desconocidos por defecto en algunas configuraciones?
Rechazar claves inesperadas (extra="forbid") detecta errores tipográficos y formas de carga inesperadas temprano, en el límite, en lugar de ignorar silenciosamente datos que el llamador pensó que se estaban utilizando.
¿Pueden dos piezas de código obtener resultados inconsistentes del mismo modelo?
Sí, si una usa el método de compatibilidad heredado .dict() y otra usa model_dump(), pueden divergir en cómo se aplican los serializadores personalizados y las conversiones específicas de JSON. Estandarizar en model_dump()/model_dump_json() evita esa deriva.
¿Cuál es la limitación honesta del modelo de validación de límites?
Solo conoce lo que está dentro de la carga útil que se está validando; no puede verificar cosas como la unicidad contra otras filas o el estado de un segundo recurso, por lo que tiene que delegar a una capa de servicio o a una restricción de base de datos para cualquier cosa que no sea autocontenida.
Relacionado
- Conceptos Básicos de Pydantic - la sintaxis práctica para definir modelos que esta página explica conceptualmente
- Rendimiento y pydantic-core - cómo se comportan el esquema compilado y el núcleo de Rust bajo carga
- Serialización -
model_dump, serializadores personalizados y la distinción del modo JSON en detalle - Validadores - mecánica de
field_validatorymodel_validator - Dataclasses vs Pydantic vs attrs - cuándo la validación de límites vale la pena el sobrecoste frente a una dataclass simple
Versiones de la pila: Esta página fue escrita para Python 3.14.0 (estable 3.14, mantenimiento 3.13) y Pydantic 2.