python-pydantic

BaseSettings y Entorno

Gestiona la configuración de tu aplicación cargando variables de entorno y archivos dotenv. BaseSettings facilita el tipado estricto de configuraciones para que los despliegues sean seguros y robustos.

Este capítulo aborda la gestión de configuraciones mediante BaseSettings, una extensión clave de Pydantic que mapea variables de entorno directamente a objetos fuertemente tipados. A lo largo de estas secciones, aprenderás a separar el código de la configuración de forma segura y robusta.

Exploraremos el funcionamiento de las variables de entorno, la integración nativa y prioridades de carga con archivos .env, y las técnicas avanzadas para organizar sistemas complejos mediante el uso de prefijos globales y estructuras jerárquicas anidadas. Es la guía definitiva para lograr despliegues limpios y reactivos bajo el principio fail-fast.

15.1. Introducción a BaseSettings

En el desarrollo de aplicaciones de software, gestionar la configuración (las credenciales de bases de datos, las claves secretas de APIs o los puertos de red) de manera segura y ordenada es un desafío crítico. El enfoque estándar y recomendado por metodologías modernas consiste en desacoplar el código de la configuración almacenando esta última en variables de entorno.

Pydantic resuelve esta necesidad mediante una extensión especializada llamada BaseSettings. Esta clase permite mapear variables de entorno directamente a un objeto fuertemente tipado, aplicando las mismas reglas de validación, coerción de tipos y valores por defecto que se utilizan en los modelos estándar de Pydantic (BaseModel).

Separación de Paquetes en Pydantic V2

A partir de Pydantic V2, la funcionalidad de gestión de configuración se extrajo del núcleo de la biblioteca principal para mantener el framework ligero. Ahora reside en un paquete independiente y dedicado llamado pydantic-settings.

Para utilizar BaseSettings, es obligatorio instalar este complemento en el entorno de desarrollo:

Bash
pip install pydantic-settings

Arquitectura Conceptual: De Entorno a Objeto

A diferencia de un BaseModel tradicional, que espera recibir los datos de manera explícita en su constructor (como un diccionario o un JSON proveniente de una petición HTTP), BaseSettings invierte la responsabilidad de la carga de datos. Al instanciarse sin argumentos, el objeto interroga al sistema operativo para extraer los valores correspondientes.

El siguiente diagrama en texto plano ilustra el flujo de inicialización y validación:

TEXT
Variables de Entorno (OS) Código Python +-----------------------+ +----------------------------------------+ | DATABASE_URL="..." | --> | class Settings(BaseSettings): | | PORT="8080" | | database_url: str | | DEBUG="true" | | port: int | +-----------------------+ | debug: bool | +----------------------------------------+ | v Instanciación: settings = Settings() | v Objeto Validado y Tipado (Memoria) +----------------------------------------+ | settings.port # -> 8080 (int) | | settings.debug # -> True (bool) | +----------------------------------------+

Tu primer modelo de configuración

Para implementar esta solución, se define una clase que hereda de BaseSettings. Los atributos de la clase se nombran en estricta coincidencia (ignorando mayúsculas y minúsculas por defecto) con las variables de entorno del sistema.

A continuación se presenta un script funcional que demuestra la carga, la coerción de tipos automática (de texto a entero o booleano) y la protección ante datos corruptos.

Python
import os from pydantic import ValidationError from pydantic_settings import BaseSettings # 1. Definición de la estructura de configuración requerida por la app class AppSettings(BaseSettings): app_name: str = "Mi Aplicación Pydantic" # Valor por defecto si no existe en el entorno api_port: int # Obligatorio: debe ser convertible a entero debug_mode: bool = False # Convierte automáticamente "true", "1", "yes" a True # 2. Simulamos la existencia de variables de entorno en el Sistema Operativo os.environ["API_PORT"] = "8000" os.environ["DEBUG_MODE"] = "True" try: # Instanciación limpia: busca automáticamente en os.environ settings = AppSettings() print("¡Configuración cargada con éxito!") print(f"Aplicación: {settings.app_name}") print(f"Puerto (Tipo: {type(settings.api_port)}): {settings.api_port}") print(f"Debug (Tipo: {type(settings.debug_mode)}): {settings.debug_mode}") except ValidationError as e: print("Error al validar las variables de entorno:") print(e.json()) # 3. Demostración de robustez: ¿Qué pasa si el entorno tiene un dato inválido? print("\n--- Simulando un error en el entorno ---") os.environ["API_PORT"] = "no-es-un-numero" try: config_invalida = AppSettings() except ValidationError as error: print("Pydantic detuvo la ejecución antes de levantar una app mal configurada:") for err in error.errors(): print(f"Campo [{err['loc'][0]}]: {err['msg']}")

Ventajas de BaseSettings frente al parseo manual

El uso de BaseSettings proporciona beneficios estructurales inmediatos en comparación con el uso directo de os.environ.get():

  • Coerción de Tipos Inteligente: Las variables de entorno son inherentemente cadenas de texto (str). BaseSettings analiza el tipo asignado al atributo y transforma el texto de manera segura (por ejemplo, interpreta la cadena "8000" como el entero 8000, y el texto "false" o "0" como el booleano False).
  • Validación en el Arranque (Fail-Fast): Si una variable crítica falta o tiene un formato incorrecto, la aplicación lanza un ValidationError inmediatamente durante la inicialización del sistema. Esto evita que los fallos de configuración se detecten tarde en entornos de producción.
  • Valores Predeterminados: Permite definir configuraciones base para entornos locales de desarrollo que se sobrescriben sin modificar el código fuente cuando la aplicación se despliega en producción o staging.

15.2. Variables de entorno

El mecanismo primordial de BaseSettings consiste en interrogar el entorno del sistema operativo para poblar sus atributos. Para dominar el uso de esta herramienta, es fundamental comprender con precisión cómo Pydantic busca las variables, cómo gestiona la sensibilidad a mayúsculas y minúsculas, y de qué manera procesa estructuras de datos complejas a partir de simples cadenas de texto.

Búsqueda y coincidencia de nombres

Por defecto, cuando instancias una clase que hereda de BaseSettings, Pydantic inspecciona el diccionario os.environ. La resolución de nombres sigue una regla de coincidencia insensible a mayúsculas y minúsculas (case-insensitive).

Si en tu modelo defines un atributo llamado database_url, Pydantic buscará exitosamente cualquiera de las siguientes variables en el entorno:

  • DATABASE_URL (Convención estándar en sistemas Unix/Docker)
  • database_url
  • Database_Url

En caso de que existan múltiples variantes en el entorno simultáneamente (por ejemplo, DATABASE_URL y database_url), Pydantic seleccionará la primera que encuentre según el orden interno del diccionario del sistema operativo, por lo que se recomienda mantener la consistencia.

Coerción de tipos complejos

Una de las grandes ventajas de BaseSettings es su capacidad para deserializar tipos de datos complejos que van más allá de primitivos como int o bool. Si un atributo se declara como una lista (list), un conjunto (set), una tupla (tuple) o un diccionario (dict), Pydantic intentará parsear la cadena de texto del entorno como un objeto JSON.

El siguiente diagrama ilustra el proceso de transformación que ejecuta internamente la biblioteca:

TEXT
Entorno (Texto Plano) Procesamiento Atributo del Objeto +---------------------------+ +--------------------------+ +---------------------------+ | ALLOWED_HOSTS='["localhost"| ------> | Detecta formato JSON | ---> | settings.allowed_hosts | | ,"127.0.0.1"]' | | Valida tipos internos | | Tipo: list[str] | +---------------------------+ +--------------------------+ +---------------------------+ +---------------------------+ +--------------------------+ +---------------------------+ | CORREOS_ADMIN='["admin@me.| ------> | Intenta parsear JSON | ---> | settings.correos_admin | | com"]' | | Si falla, separa por ',' | | Tipo: set[str] | +---------------------------+ +--------------------------+ +---------------------------+

Nota de comportamiento para colecciones (list, set, tuple): Si la cadena de texto en el entorno no es un JSON válido (por ejemplo, ALLOWED_HOSTS=localhost,127.0.0.1), Pydantic V2 adoptará un comportamiento de respaldo y dividirá la cadena utilizando la coma (,) como delimitador. Sin embargo, para diccionarios (dict), el valor del entorno debe ser obligatoriamente un objeto JSON válido.

Demostración Práctica: Tipado Estricto del Entorno

El siguiente fragmento de código demuestra cómo configurar un entorno complejo y cómo BaseSettings procesa las variables, maneja la insensibilidad a mayúsculas y deserializa estructuras JSON y tipos especiales como SecretStr (visto en la sección 13.5) para proteger datos sensibles en los logs.

Python
import os from typing import SecretStr from pydantic_settings import BaseSettings class ServerSettings(BaseSettings): # Insensible a mayúsculas/minúsculas: coincidirá con HOST o host host: str = "0.0.0.0" # Coerción de JSON a tipos estructurados de Python allowed_ports: list[int] api_timeouts: dict[str, float] # Uso de tipos especiales de Pydantic para mayor seguridad auth_token: SecretStr # 1. Configuración del entorno simulando el sistema operativo # Nota cómo usamos mayúsculas estándar y estructuras JSON en formato de texto os.environ["HOST"] = "127.0.0.1" os.environ["ALLOWED_PORTS"] = "[80, 443, 8080]" os.environ["API_TIMEOUTS"] = '{"connect": 3.5, "read": 10.0}' os.environ["AUTH_TOKEN"] = "super-secreto-123" # 2. Inicialización del objeto de configuración settings = ServerSettings() print("--- Configuración de Servidor Cargada ---") print(f"Host detectado (OS de origen 'HOST'): {settings.host}") print(f"Puertos permitidos: {settings.allowed_ports} -> Tipo: {type(settings.allowed_ports)}") print(f"Primer puerto: {settings.allowed_ports[0]} -> Tipo interno: {type(settings.allowed_ports[0])}") print(f"Timeouts del sistema: {settings.api_timeouts} -> Tipo: {type(settings.api_timeouts)}") print(f"Timeout de lectura: {settings.api_timeouts['read']}s") # 3. Seguridad de datos sensibles print(f"Token de autenticación (ofuscado en print): {settings.auth_token}") print(f"Token real (revelado explícitamente): {settings.auth_token.get_secret_value()}")

Prioridad de valores

Al construir una instancia de BaseSettings, es posible pasar argumentos explícitos a través del constructor. Cuando esto sucede, los valores provistos de forma directa en el código tienen prioridad absoluta sobre los valores presentes en las variables de entorno del sistema operativo.

El orden de evaluación de Pydantic para determinar el valor final de un atributo sigue estrictamente la siguiente jerarquía:

  1. Valores pasados explícitamente al instanciar la clase: Settings(host="192.168.1.1").
  2. Variables de entorno leídas del sistema operativo (e.g. os.environ["HOST"]).
  3. Valores asignados por defecto en la definición de la clase (e.g. host: str = "0.0.0.0").

15.3. Carga desde archivos .env

En entornos de desarrollo local o testing, gestionar decenas de variables de entorno directamente en la terminal del sistema operativo puede volverse ineficiente y propenso a errores. La práctica estándar de la industria consiste en almacenar estas variables en un archivo de texto plano llamado .env.

BaseSettings ofrece soporte nativo para leer estos archivos automáticamente, parsear sus componentes y combinarlos con las variables del sistema operativo, todo sin necesidad de añadir librerías externas de terceros como python-dotenv.

Dependencia adicional requerida

Para que Pydantic pueda procesar archivos .env, el paquete pydantic-settings necesita la librería de parseo dotenv-python. Puedes asegurarte de tenerla instalada ejecutando el comando de instalación con el extra de dependencias correspondiente:

Bash
pip install "pydantic-settings[file-providers]"

Configuración mediante SettingsConfigDict

Para indicarle a un modelo BaseSettings que debe leer un archivo .env, se utiliza el atributo especial de clase model_config alimentado por la clase SettingsConfigDict. Esta clase hereda y extiende las capacidades de ConfigDict (analizado en la sección 12.1).

A continuación se detalla un diagrama de flujo que describe cómo Pydantic interactúa con el sistema de archivos y la memoria del sistema operativo al inicializarse:

TEXT
+-----------------------------------+ | Instanciación: Settings() | +-----------------------------------+ | v +-----------------------------------+ | ¿Existe variable en os.environ? | +-----------------------------------+ / \ SÍ / \ NO v v +-----------------------------------+ +-----------------------------------+ | Usa el valor de os.environ | | Busca en el archivo físico .env | | (El sistema operativo manda) | +-----------------------------------+ +-----------------------------------+ | v +-----------------------------------+ | ¿Se encontró la clave en .env? | +-----------------------------------+ / \ SÍ / \ NO v v +------------------------------+ +------------------------------+ | Aplica el valor del .env | | Usa el valor por defecto | | | | definido en el código Python | +------------------------------+ +------------------------------+

Regla de oro de la prioridad: Las variables de entorno reales del sistema operativo (os.environ) siempre sobrescriben a las variables declaradas en el archivo .env. Esto permite que un sistema de Integración Continua (CI) o un contenedor Docker inyecte configuraciones dinámicas en producción sin necesidad de modificar el archivo .env de desarrollo.

Implementación Práctica

El siguiente ejemplo simula la creación de un archivo .env en el disco local y demuestra cómo configurar Pydantic para consumirlo, manejar la codificación de caracteres y definir rutas alternativas.

Python
from pydantic import PostgresDsn from pydantic_settings import BaseSettings, SettingsConfigDict # 1. Simulación física de un archivo de configuración ".env" # En un proyecto real, este archivo se crea a mano en la raíz del proyecto. contenido_env = """ # Configuración del Entorno de Desarrollo DEBUG_MODE=true DB_URL=postgresql://usuario:clave_secreta@localhost:5432/mi_base_de_datos MAX_CONNECTIONS=20 """ with open(".env", "w", encoding="utf-8") as f: f.write(contenido_env.strip()) # 2. Definición del modelo Pydantic preparado para consumir el archivo class DatabaseSettings(BaseSettings): # Declaramos la configuración global del modelo de settings model_config = SettingsConfigDict( env_file=".env", # Nombre o ruta del archivo a leer env_file_encoding="utf-8", # Codificación para evitar fallos con caracteres especiales extra="ignore" # Ignora variables sobrantes en el .env que no use la clase ) debug_mode: bool max_connections: int = 10 # Si no estuviera en el .env, usaría 10 db_url: PostgresDsn # Tipo especial validado de Pydantic (sección 13.2) # 3. Carga y verificación try: settings = DatabaseSettings() print("--- Configuración Cargada desde .env ---") print(f"Modo Debug: {settings.debug_mode} (Tipo: {type(settings.debug_mode)})") print(f"Conexiones Máximas: {settings.max_connections}") print(f"Host de Base de Datos: {settings.db_url.host}") print(f"Esquema de Conexión: {settings.db_url.scheme}") except Exception as e: print(f"Error al procesar la configuración: {e}")

Carga de múltiples archivos .env (Multi-environment)

Una característica avanzada de SettingsConfigDict en Pydantic V2 es la capacidad de aceptar una tupla de rutas en la propiedad env_file. Pydantic leerá los archivos en el orden indicado, donde los archivos posteriores sobrescriben los valores de los archivos anteriores.

Esta técnica es ideal para manejar configuraciones base y sobreescribirlas según el entorno de ejecución:

Python
class AdvancedSettings(BaseSettings): model_config = SettingsConfigDict( # Lee primero la base común. Si una variable se repite en .env.local, # prevalece el valor de .env.local env_file=(".env.shared", ".env.local"), env_file_encoding="utf-8" ) api_key: str environment_name: str

15.4. Prefijos y jerarquías

Cuando una aplicación crece, el número de variables de entorno aumenta exponencialmente. Dejar todas las variables en el espacio global del sistema sin ningún orden incrementa el riesgo de colisiones de nombres (por ejemplo, que la variable PORT de tu microservicio choque con la variable PORT del sistema operativo).

Para solucionar esto, BaseSettings introduce dos herramientas organizativas avanzadas: el uso de prefijos globales y el modelado de estructuras jerárquicas anidadas.

Uso de prefijos con env_prefix

La propiedad env_prefix dentro de SettingsConfigDict indica a Pydantic que solo debe buscar variables de entorno que comiencen con una cadena de texto específica. Al realizar la coincidencia, Pydantic busca el prefijo en el entorno pero lo remueve al asignar el valor al atributo de la clase Python.

Por ejemplo, si configuras env_prefix="PROD_", la variable de entorno PROD_DB_USER se mapeará limpiamente al atributo db_user del modelo.

TEXT
Variables de Entorno Procesamiento Modelo Pydantic +-----------------------+ +----------------------------+ +-------------------------+ | PROD_HOST="10.0.0.5" | --> | 1. Detecta prefijo "PROD_" | --> | class ProdSettings: | | PROD_PORT="9000" | | 2. Remueve el prefijo | | host: str #10.0.0.5| | STAGING_PORT="5000" | | 3. Mapea al atributo | | port: int #9000 | +-----------------------+ +----------------------------+ +-------------------------+ | ^ +----------------------- (Ignorada por no usar el prefijo) ------------+

Configuraciones Jerárquicas Anidadas

Para sistemas complejos, almacenar todas las configuraciones en un único modelo plano rompe el principio de responsabilidad única. Pydantic permite anidar modelos estándar (BaseModel) o submodelos de configuración dentro de una clase BaseSettings principal.

A partir de Pydantic V2, el comportamiento por defecto para poblar un submodelo desde el entorno consiste en buscar un objeto JSON estructurado bajo el nombre del atributo padre, o bien utilizar un delimitador (doble guión bajo por defecto: __) para separar de forma jerárquica los niveles de configuración en el entorno.

Implementación Práctica Completa

El siguiente código fuente demuestra de manera combinada cómo aislar configuraciones usando prefijos y cómo estructurar una arquitectura de configuración dividida en submodelos funcionales (Base de datos y Seguridad) utilizando la separación por doble guión bajo (__).

Python
import os from pydantic import BaseModel, Field from pydantic_settings import BaseSettings, SettingsConfigDict # 1. Definimos submodelos estándar para agrupar lógica relacionada class DatabaseConfig(BaseModel): host: str = "localhost" port: int = 5432 user: str class SecurityConfig(BaseModel): secret_key: str token_ttl_seconds: int = 3600 # 2. Modelo principal de Settings con prefijo y delimitador jerárquico class AppSettings(BaseSettings): model_config = SettingsConfigDict( env_prefix="APP_", # Obliga a que todo empiece con APP_ env_nested_delimiter="__", # Delimitador para recorrer la jerarquía extra="ignore" ) # Atributo global de primer nivel environment: str = "production" # Submodelos jerárquicos db: DatabaseConfig auth: SecurityConfig # 3. Simulamos las variables de entorno inyectadas en el sistema operativo # Estructura: PREFIJO + NOMBRE_SUBMODELO + DELIMITADOR + ATRIBUTO os.environ["APP_ENVIRONMENT"] = "staging" os.environ["APP_DB__USER"] = "admin_user" os.environ["APP_DB__PORT"] = "9999" # Sobrescribe el valor por defecto (5432) os.environ["APP_AUTH__SECRET_KEY"] = "token-de-alta-seguridad" # 4. Carga y lectura limpia de la configuración estructurada try: settings = AppSettings() print("--- Configuración Jerárquica Cargada con éxito ---") print(f"Entorno General: {settings.environment}") print("\n[Módulo de Base de Datos]") print(f" Usuario: {settings.db.user}") print(f" Host (Por defecto): {settings.db.host}") print(f" Puerto (Sobrescrito): {settings.db.port}") print("\n[Módulo de Seguridad]") print(f" Tiempo de vida del token: {settings.auth.token_ttl_seconds} segundos") print(f" Llave secreta disponible: {settings.auth.secret_key is not None}") except Exception as e: print(f"Error de validación: {e}")

Resumen del capítulo

En este capítulo hemos explorado en profundidad las capacidades de la extensión BaseSettings (provista por el paquete pydantic-settings) como la solución definitiva de Pydantic para la gestión de configuraciones seguras en entornos profesionales de desarrollo.

  • Fundamento y Propósito: BaseSettings hereda el motor de validación de Pydantic para cargar configuraciones desde el entorno del sistema operativo a objetos fuertemente tipados con coerción de datos e inicialización rápida (fail-fast).
  • Variables del Sistema: Aprendimos el comportamiento agnóstico de la biblioteca respecto a mayúsculas y minúsculas y su capacidad para deserializar tipos complejos como listas y diccionarios directamente desde texto o formato JSON.
  • Archivos Dotenv: Estudiamos la integración nativa de archivos .env mediante SettingsConfigDict, estableciendo con claridad que el entorno del sistema operativo (os.environ) siempre ejerce prioridad absoluta de sobreescritura sobre el almacenamiento en archivos planos.
  • Estructura y Escala: Analizamos el uso de env_prefix para mitigar colisiones en el sistema y el diseño de arquitecturas jerárquicas mediante submodelos de datos interconectados a través de delimitadores de variables anidadas (__).

© 2026 Esdocu. Contenido bajo licencia MIT.

Editar esta página