Concepto clave
La validación de datos es el proceso de verificar que la información que recibe tu API cumple con las reglas y formatos esperados antes de procesarla. Imagina que tu API es un restaurante de alta cocina: los datos de entrada son los ingredientes que llegan a la cocina. Pydantic actúa como el chef de calidad que inspecciona cada ingrediente antes de que entre a la cocina, rechazando los que están podridos, mal etiquetados o incompletos.
En el contexto de APIs de Machine Learning, la validación es crítica porque los modelos ML son extremadamente sensibles a la calidad y formato de los datos. Un valor numérico donde se espera texto, o un rango fuera de lo normal, puede causar predicciones erróneas o incluso fallos en producción. Pydantic te permite definir modelos de datos con tipos, restricciones y validaciones personalizadas que FastAPI usa automáticamente para validar las solicitudes entrantes.
Cómo funciona en la práctica
Cuando un cliente envía datos a tu endpoint FastAPI, el flujo de validación sigue estos pasos:
- FastAPI recibe la solicitud HTTP (por ejemplo, un POST con JSON)
- Extrae los datos del cuerpo de la solicitud
- Intenta crear una instancia del modelo Pydantic que definiste para ese endpoint
- Pydantic valida cada campo según las reglas que especificaste (tipo de dato, rangos, formatos, etc.)
- Si la validación pasa, los datos validados están disponibles en tu función de endpoint
- Si falla, FastAPI automáticamente devuelve un error 422 con detalles de qué campo falló y por qué
Este proceso ocurre antes de que tu código de ML o lógica de negocio se ejecute, protegiendo tu sistema de datos malformados.
Código en acción
Veamos un ejemplo básico de un endpoint para predecir precios de casas:
from fastapi import FastAPI
from pydantic import BaseModel, Field
from typing import Optional
app = FastAPI()
# ANTES: Sin validación estructurada
@app.post("/predict/old")
async def predict_old(
area: float,
bedrooms: int,
location: str
):
# Aquí tendrías que validar manualmente cada parámetro
if area <= 0:
return {"error": "El área debe ser positiva"}
if bedrooms < 1 or bedrooms > 10:
return {"error": "Número de habitaciones inválido"}
# ... más validaciones manuales
# Lógica del modelo ML
return {"predicted_price": 250000}
# DESPUÉS: Con Pydantic
class HouseFeatures(BaseModel):
area: float = Field(..., gt=0, description="Área construida en metros cuadrados")
bedrooms: int = Field(..., ge=1, le=10, description="Número de habitaciones")
location: str = Field(..., min_length=2, max_length=100)
has_garage: Optional[bool] = Field(True, description="Indica si tiene garaje")
year_built: int = Field(..., ge=1800, le=2024)
@app.post("/predict")
async def predict(features: HouseFeatures):
# features ya está validado automáticamente
# Todos los campos cumplen con las reglas definidas
# Ejemplo de procesamiento con modelo ML
# price = ml_model.predict([[features.area, features.bedrooms, ...]])
return {
"predicted_price": 325000,
"validated_features": features.dict()
}
Ahora un ejemplo más avanzado con validaciones personalizadas para datos de ML:
from pydantic import BaseModel, validator
import numpy as np
from typing import List
class MLInput(BaseModel):
features: List[float]
model_version: str = "v1.0"
@validator('features')
def validate_features_length(cls, v):
"""Valida que el vector de características tenga la longitud esperada"""
expected_length = 10 # Para nuestro modelo específico
if len(v) != expected_length:
raise ValueError(f"Se esperaban {expected_length} features, se recibieron {len(v)}")
return v
@validator('features')
def validate_no_nans(cls, v):
"""Valida que no haya valores NaN en los features"""
if any(np.isnan(val) for val in v):
raise ValueError("Los features no pueden contener valores NaN")
return v
@validator('model_version')
def validate_model_version(cls, v):
"""Valida que la versión del modelo sea soportada"""
supported_versions = ["v1.0", "v1.1", "v2.0"]
if v not in supported_versions:
raise ValueError(f"Versión {v} no soportada. Versiones válidas: {supported_versions}")
return v
@app.post("/ml-predict")
async def ml_predict(input_data: MLInput):
# input_data.features ya está validado:
# - Es una lista de floats
# - Tiene exactamente 10 elementos
# - No contiene NaN
# - model_version es una de las soportadas
# Tu lógica de predicción ML aquí
prediction = 0.85 # Ejemplo
return {
"prediction": prediction,
"model_used": input_data.model_version,
"features_received": len(input_data.features)
}
Errores comunes
- Validación insuficiente en producción: Solo validar tipos básicos sin considerar rangos realistas. Solución: Usa Field() con constraints como gt, lt, ge, le para valores numéricos y min_length/max_length para strings.
- Ignorar valores opcionales: No definir campos opcionales con Optional[] y default values, causando errores cuando clientes no los envían. Solución: Siempre marca campos opcionales con Optional[Tipo] = default_value.
- Validaciones duplicadas: Implementar validación manual además de Pydantic. Solución: Confía en Pydantic y centraliza toda la validación en los modelos.
- Mensajes de error poco informativos: Dejar los mensajes de error por defecto. Solución: Usa Field(..., description="...") y custom validators con mensajes claros.
- No validar formatos específicos de ML: Aceptar cualquier lista de números sin validar longitud o valores especiales como NaN/Inf. Solución: Implementa validators personalizados para requisitos específicos de tu modelo.
Checklist de dominio
- Puedo definir un modelo Pydantic con tipos básicos (int, float, str, bool, List, Dict)
- Sé usar Field() para agregar constraints como valores mínimos/máximos, descripciones y ejemplos
- Puedo crear validadores personalizados con @validator para reglas de negocio específicas
- Entiendo la diferencia entre Optional[T] con valor por defecto y campos requeridos
- Sé cómo Pydantic se integra automáticamente con FastAPI para documentación OpenAPI
- Puedo manejar errores de validación y devolver respuestas apropiadas al cliente
- Sé validar estructuras de datos específicas para modelos ML (vectores de features, rangos esperados, etc.)
Implementa validación para un modelo de clasificación de texto
En este ejercicio, crearás un endpoint FastAPI con validación Pydantic para un servicio de clasificación de sentimientos en texto.
- Crea un nuevo archivo Python llamado
sentiment_api.py - Define un modelo Pydantic llamado
TextInputcon los siguientes campos:text: string requerido, entre 1 y 1000 caractereslanguage: string opcional con valor por defecto "es", solo permitir "es" o "en"model_version: string requerido, solo permitir "v1", "v2", o "v3"confidence_threshold: float opcional entre 0.5 y 1.0, valor por defecto 0.7
- Agrega un validador personalizado que rechace texto que contenga solo espacios o caracteres especiales
- Crea un endpoint POST
/analyze-sentimentque acepteTextInput - En la función del endpoint, simula un análisis de sentimiento devolviendo:
- Un score entre -1 (negativo) y 1 (positivo)
- La categoría ("positivo", "neutral", "negativo") basado en el score
- Si la confianza supera el threshold
- Todos los datos validados recibidos
- Prueba tu API con diferentes entradas válidas e inválidas usando curl o Postman
- Usa Field() con min_length y max_length para el campo text
- Para el validador de texto no vacío, usa strip() y verifica si el resultado tiene longitud > 0
- Recuerda que los validadores personalizados deben usar @validator y raise ValueError si fallan
Evalua tu comprension
Completa el quiz interactivo de arriba para ganar XP.