Saltar al contenido
🐍 Curso de Python
/

Módulo 5 · Lección 25 de 30

Pydantic y validación

Lo que convierte en silencio, la palabra yes que vale y sí que no, y la línea que arregla el fallo de la lección 13.

⏱ 16 min de lectura

Pydantic es lo que hace funcionar a FastAPI, y se puede usar solo: para leer ficheros de configuración, validar mensajes de una cola o dar forma a la respuesta de otra API, que es el bloque A.

Lo que hay que entender de él es qué convierte sin preguntar, porque es generoso y hay conversiones que sorprenden.

Lo que convierte#

Medido, con el comportamiento de fábrica:

Salida en pantalla
n='42'          -> 42
n='42.0'        -> 42
n=42.0          -> 42
n=42.7          -> ERROR: Input should be a valid integer, got a number
n=True          -> 1

b='true'        -> True
b='1'           -> True
b=1             -> True
b='yes'         -> True
b='si'          -> ERROR: Input should be a valid boolean

d='2026-01-15'  -> date(2026, 1, 15)
d=1768435200    -> date(2026, 1, 15)
d='15/01/2026'  -> ERROR: Input should be a valid date or datetime

dec='19.98'     -> Decimal('19.98')
dec=19.98       -> Decimal('19.98')

Cuatro cosas de ahí merecen comentario.

42.7 da error y 42.0 no. Es la decisión correcta: convertir cuando no se pierde nada y protestar cuando sí. Es lo contrario de lo que hace int() de Python, que trunca en silencio.

True se convierte en 1. Esta sí es una conversión discutible, y es la que hay que vigilar: si tu campo es un entero y alguien manda true, recibes un uno sin enterarte.

Un número entero se convierte en fecha, interpretándolo como marca de tiempo Unix. Cómodo y significa que un identificador mandado por error en un campo de fecha produce una fecha plausible en lugar de un error.

Y el decimal conserva lo que se ve, incluso viniendo de una coma flotante, porque pydantic convierte pasando por texto. Es la buena noticia de la lección 3: 19.98 no se convierte en 19.979999999999997. Aun así, el dinero se manda como texto, porque lo que llegue ya vendrá roto si el cliente lo mandó como número.

Laxo o estricto#

Lo de arriba es el modo laxo, que es el de fábrica. El otro no convierte nada:

python
from pydantic import ConfigDict


class Estricta(BaseModel):
        model_config = ConfigDict(strict=True)
    n: int
    b: bool
Salida en pantalla
n='42'    -> ERROR: Input should be a valid integer
n=42      -> ok
b='true'  -> ERROR: Input should be a valid boolean
b=True    -> ok

Cuál usar depende de por dónde entran los datos:

Con JSON, el laxo tiene sentido, porque JSON no distingue enteros de decimales igual que Python y muchos clientes mandan números como texto. Rechazar "42" en un campo entero produce discusiones que no llevan a nada.

Con datos internos, el estricto. Mensajes entre tus propios servicios, ficheros de configuración, lo que sale de tu base de datos: ahí una conversión inesperada esconde un error real.

Y se puede pedir por campo, que suele ser la respuesta correcta:

python
class Pedido(BaseModel):
        cantidad: int
    es_regalo: Annotated[bool, Field(strict=True)]

Así el número aguanta lo que llegue y el booleano no acepta el 1 que en realidad era otra cosa.

Los validadores#

Tres niveles, como en DRF, y aquí con un matiz de más:

python
from pydantic import field_validator, model_validator


class Reserva(BaseModel):
        titulo: str = Field(max_length=10)
    desde: date
    hasta: date

    @field_validator('titulo', mode='before')
    @classmethod
    def quitar_espacios(cls, valor):
                return valor.strip() if isinstance(valor, str) else valor

    @field_validator('titulo')
    @classmethod
    def sin_articulo(cls, valor):
                if valor.lower().startswith('el '):
                        raise ValueError('No empieces por artículo.')
        return valor.title()

    @model_validator(mode='after')
    def fechas_coherentes(self):
                if self.desde > self.hasta:
                        raise ValueError('«hasta» debe ser posterior a «desde».')
        return self

El orden, medido:

Salida en pantalla
entrada '  holaaa  ':  ['before', 'after', 'model']  ->  'Holaaa'

El previo corre antes de convertir el tipo y el otro después, y esa es la diferencia que no existe en DRF. Sirve para casos concretos:

python
@field_validator('etiquetas', mode='before')
@classmethod
def admitir_texto(cls, valor):
        """Acepta "a,b,c" además de ["a", "b", "c"]."""
    if isinstance(valor, str):
                return [x.strip() for x in valor.split(',')]
    return valor

Eso no se puede hacer después, porque para entonces el texto ya habría fallado al no ser una lista.

Y la regla de la lección 13 se mantiene igual:

Salida en pantalla
con el título demasiado largo:  ['before']
  errores: [(('titulo',), 'String should have at most 10 characters')]
  -> el validador del modelo NI SE EJECUTA

con el título correcto:  ['before', 'after', 'model']
  errores: [((), 'Value error, «hasta» debe ser posterior a «desde».')]