Image for post Gestión de Configuración Moderna en Python con Pydantic-Settings

Gestión de Configuración Moderna en Python con Pydantic-Settings


Contexto del Problema

Como desarrollador Python, seguramente te has enfrentado al desafío de gestionar la configuración de tus aplicaciones. Al principio, es tentador "hardcodear" valores como claves de API, URLs de bases de datos o constantes de negocio directamente en el código. Sin embargo, esta práctica se vuelve insostenible rápidamente.

¿Qué sucede cuando necesitas desplegar tu aplicación en un entorno de pruebas (staging) o producción? ¿O si un compañero necesita ejecutar el proyecto en su máquina local con una base de datos diferente? Cambiar el código fuente cada vez es ineficiente y propenso a errores. Podrías terminar subiendo accidentalmente una clave secreta a un repositorio de Git, un error de seguridad grave.

El siguiente paso evolutivo suele ser usar variables de entorno con os.getenv(). Esto es mucho mejor, ya que separa la configuración del código, siguiendo principios como los de la aplicación de 12 factores. Pero incluso este enfoque tiene sus limitaciones:

  • Falta de tipado: os.getenv() siempre devuelve strings (o None). Tienes que convertir manualmente valores a enteros, booleanos o listas, lo que añade código repetitivo y posibles errores de conversión.
  • Sin validación: No hay una forma integrada de asegurar que una variable de entorno exista o que su valor sea válido (p. ej., una URL bien formada) antes de que la aplicación intente usarla, lo que puede causar fallos en tiempo de ejecución.
  • Gestión engorrosa: Para el desarrollo local, gestionar múltiples variables de entorno puede ser complicado. Los archivos .env ayudan, pero requieren una biblioteca adicional para cargarlos.

Aquí es donde entra en juego pydantic-settings, una librería que resuelve estos problemas de una manera elegante, robusta y muy "pythónica".

Conceptos Clave

Antes de sumergirnos en el código, aclaremos algunos conceptos fundamentales.

  • Pydantic: Es una biblioteca de validación de datos y gestión de configuraciones que utiliza anotaciones de tipo (type hints) de Python. Su principal ventaja es que impone el tipado en tiempo de ejecución, proporcionando errores claros y descriptivos cuando los datos no cumplen con el esquema definido.
  • pydantic-settings: Es un componente del ecosistema Pydantic, enfocado específicamente en la gestión de configuraciones. Permite definir tus ajustes en una clase, combinando la potencia de la validación de Pydantic con la capacidad de leer valores desde múltiples fuentes, como variables de entorno y archivos .env.
  • Configuración declarativa y con tipos seguros (Type-Safe): En lugar de obtener valores imperativamente y convertirlos manualmente, declaras una clase que representa tu configuración. Cada atributo de la clase tiene un tipo definido (str, int, bool, HttpUrl, etc.). La librería se encarga de leer, convertir y validar los valores por ti. Esto reduce el código repetitivo y previene una categoría entera de errores.
  • Archivos .env: Son archivos de texto plano que almacenan variables de entorno en formato CLAVE=VALOR. Son un estándar de facto para gestionar la configuración en desarrollo local, ya que permiten definir todas las variables necesarias en un solo lugar y mantenerlo fuera del control de versiones.

Implementación Paso a Paso

Vamos a construir una configuración robusta desde cero. Verás lo sencillo que es.

1. Instalación

Primero, necesitas instalar la librería. pydantic-settings se encarga de instalar también pydantic y python-dotenv como dependencias.

pip install pydantic-settings

2. Creando tu primera clase de configuración

Imagina que nuestra aplicación necesita una clave de API, un modo de depuración y la cantidad máxima de reintentos para una conexión.

Crea un archivo llamado config.py:

from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    # Atributo requerido (fallará si no se encuentra)
    API_KEY: str

    # Atributo con valor por defecto
    DEBUG_MODE: bool = False

    # Atributo con tipo específico y valor por defecto
    MAX_RETRIES: int = 3

    # Configuración del modelo para indicar de dónde leer
    model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")

# Instancia global que usaremos en la aplicación
settings = Settings()

Analicemos este código. Heredamos de BaseSettings, que es la clase mágica que orquesta todo. Definimos nuestros campos con anotaciones de tipo. API_KEY: str no tiene un valor por defecto, lo que la convierte en obligatoria. Si pydantic-settings no la encuentra, lanzará una excepción ValidationError, deteniendo la aplicación antes de que falle en un punto inesperado. DEBUG_MODE y MAX_RETRIES tienen valores por defecto.

La clase anidada SettingsConfigDict (anteriormente Config) le dice a Pydantic que busque un archivo llamado .env para cargar las variables.

3. Creando el archivo .env

En la misma carpeta donde está config.py, crea un archivo llamado .env. ¡Importante! Asegúrate de añadir .env a tu archivo .gitignore para no subirlo nunca a tu repositorio.

# No uses comillas para los strings, a menos que contengan espacios
API_KEY="una-clave-secreta-muy-larga-y-segura"

# pydantic-settings convierte automáticamente 'true', '1', 'on' a True
DEBUG_MODE=true

# Este valor sobreescribirá el defecto de 3
MAX_RETRIES=5

4. Usando la configuración en tu aplicación

Ahora, en cualquier otra parte de tu proyecto, puedes importar y usar la instancia settings.

Crea un archivo main.py:

from config import settings

def connect_to_api():
    print(f"Conectando a la API con la clave: ...{settings.API_KEY[-4:]}")
    for i in range(settings.MAX_RETRIES):
        print(f"Intento de conexión #{i + 1}")
        # Aquí iría la lógica de conexión real

if __name__ == "__main__":
    print(f"Iniciando la aplicación...")
    if settings.DEBUG_MODE:
        print("¡Atención! El modo de depuración está activado.")
    
    connect_to_api()
    
    print("\n--- Configuración cargada ---")
    # .model_dump() es útil para ver toda la configuración
    print(settings.model_dump())

5. Ejecución y prueba

Abre tu terminal en el directorio del proyecto y ejecuta main.py:

python main.py

La salida debería ser:

Iniciando la aplicación...
¡Atención! El modo de depuración está activado.
Conectando a la API con la clave: ...gura"
Intento de conexión #1
Intento de conexión #2
Intento de conexión #3
Intento de conexión #4
Intento de conexión #5

--- Configuración cargada ---
{'API_KEY': 'una-clave-secreta-muy-larga-y-segura', 'DEBUG_MODE': True, 'MAX_RETRIES': 5}

¡Felicidades! Has creado una configuración tipada, validada y cargada desde un archivo .env. Observa cómo DEBUG_MODE se convirtió a un booleano True y MAX_RETRIES a un entero 5, todo automáticamente.

Mini Proyecto / Aplicación Sencilla

Vamos a aplicar lo aprendido a un caso un poco más realista: un cliente que se conecta a una base de datos y a una API externa. Esto nos permitirá ver cómo organizar configuraciones anidadas.

1. Estructura de configuración anidada

Modifica tu archivo config.py para que sea más modular.

from pydantic import BaseModel, PostgresDsn
from pydantic_settings import BaseSettings, SettingsConfigDict

# Modelo para la configuración de la base de datos
# Hereda de BaseModel porque no carga variables directamente
class DatabaseSettings(BaseModel):
    URL: PostgresDsn  # Tipo especial de Pydantic para DSN de PostgreSQL
    POOL_SIZE: int = 10

# Modelo para la configuración de la API externa
class ApiSettings(BaseModel):
    KEY: str
    TIMEOUT: int = 30

# Clase principal de configuración
class Settings(BaseSettings):
    APP_NAME: str = "Mi Aplicación Increíble"
    DEBUG_MODE: bool = False
    
    # Campos anidados
    DB: DatabaseSettings
    EXTERNAL_API: ApiSettings

    model_config = SettingsConfigDict(
        env_file=".env",
        env_nested_delimiter='__', # Delimitador para variables anidadas
        env_file_encoding="utf-8"
    )

settings = Settings()

Hemos introducido BaseModel para las clases anidadas y un tipo especializado, PostgresDsn, que validará que la URL de la base de datos tenga el formato correcto. El env_nested_delimiter='__' es clave: le dice a Pydantic cómo mapear variables de entorno a los modelos anidados. Por ejemplo, la variable DB__URL se mapeará a settings.DB.URL.

2. Actualiza tu archivo .env

APP_NAME="Cliente de Datos v2"
DEBUG_MODE=1

# Variables para el modelo anidado de base de datos
DB__URL="postgresql://user:password@localhost:5432/mydatabase"
DB__POOL_SIZE=20

# Variables para el modelo anidado de la API externa
EXTERNAL_API__KEY="otra-clave-secreta-para-la-api"
EXTERNAL_API__TIMEOUT=45

3. Actualiza main.py para usar la nueva estructura

from config import settings

def initialize_database_pool():
    print("Inicializando pool de conexiones de la base de datos...")
    print(f"  URL: {settings.DB.URL.unicode_string()}")
    print(f"  Tamaño del Pool: {settings.DB.POOL_SIZE}")

def fetch_data_from_external_api():
    print("\nObteniendo datos de la API externa...")
    print(f"  Clave de API: ...{settings.EXTERNAL_API.KEY[-4:]}")
    print(f"  Timeout: {settings.EXTERNAL_API.TIMEOUT} segundos")

if __name__ == "__main__":
    print(f"Iniciando: {settings.APP_NAME}")
    if settings.DEBUG_MODE:
        print("  -> Modo depuración: ACTIVADO")

    initialize_database_pool()
    fetch_data_from_external_api()

    print("\n--- Configuración completa ---")
    print(settings.model_dump_json(indent=2))

Al ejecutar este nuevo main.py, verás cómo la configuración se carga de forma estructurada y validada, haciendo tu código más limpio y organizado.

Errores Comunes y Depuración

  • ValidationError al iniciar: Es el error más común. Significa que una variable requerida no fue encontrada o que el valor proporcionado no se pudo convertir al tipo esperado. Revisa tu archivo .env y las variables de entorno. Asegúrate de que los nombres coincidan (Pydantic no distingue mayúsculas de minúsculas por defecto) y que los valores sean correctos (p. ej., no poner "abc" para un campo int).
  • El archivo .env no se carga: Verifica que el nombre del archivo en SettingsConfigDict sea correcto y que el archivo esté en el directorio desde donde ejecutas el script. Si ejecutas desde un subdirectorio, la ruta podría no ser la correcta.
  • Prioridad de las fuentes: Las variables de entorno del sistema operativo siempre tienen prioridad sobre las definidas en el archivo .env. Si cambias un valor en .env y no se refleja, es probable que tengas esa variable definida en tu terminal. Puedes usar echo $NOMBRE_VARIABLE (Linux/macOS) o echo %NOMBRE_VARIABLE% (Windows) para verificar.
  • Variables anidadas no funcionan: Asegúrate de haber configurado env_nested_delimiter y de que tus variables en el .env usan ese delimitador (p. ej., DB__URL).

Aprendizaje Futuro / Próximos Pasos

pydantic-settings es una herramienta muy potente y lo que hemos visto es solo el comienzo. Aquí tienes algunas ideas para seguir explorando:

  • Prefijos de entorno: Puedes configurar un env_prefix en SettingsConfigDict para que todas las variables de entorno de tu aplicación deban empezar con un prefijo, por ejemplo MYAPP_. Esto es útil para evitar colisiones en sistemas con muchas variables.
  • Validadores personalizados: Puedes usar los validadores de Pydantic para añadir lógica de validación compleja. Por ejemplo, asegurar que si DEBUG_MODE es False, entonces una variable LOG_LEVEL no puede ser 'DEBUG'.
  • Fuentes de configuración personalizadas: Además de variables de entorno y archivos .env, puedes extender pydantic-settings para leer configuraciones desde archivos TOML, YAML, o incluso desde servicios de gestión de secretos como AWS Secrets Manager o HashiCorp Vault.
  • Integración con FastAPI: FastAPI se integra de manera nativa y excepcional con Pydantic. Puedes usar tu clase de configuración para gestionar los ajustes de tu API de una forma limpia y eficiente, a menudo usando un sistema de inyección de dependencias.

Adoptar una estrategia de configuración robusta desde el inicio de tus proyectos te ahorrará incontables horas de depuración y facilitará enormemente el mantenimiento y despliegue de tus aplicaciones. pydantic-settings te ofrece el equilibrio perfecto entre simplicidad y potencia para lograrlo.