Variables de entorno y configuración en Python: dotenv, os.environ y archivos config
La gestión de la configuración es fundamental para crear aplicaciones seguras y portables. El principio básico: no codifiques credenciales ni URLs de base de datos en el código fuente; usa variables de entorno o archivos de configuración externos.
1. Variables de entorno con os.environ
import os
# Leer una variable de entorno
db_url = os.environ.get("DATABASE_URL")
if db_url is None:
raise RuntimeError("DATABASE_URL no está configurada")
# Valor por defecto si no existe
debug = os.environ.get("DEBUG", "false").lower() == "true"
port = int(os.environ.get("PORT", "8000"))
print(f"DB: {db_url}")
print(f"Debug: {debug}, Puerto: {port}")
# Listar todas las variables de entorno
for clave, valor in sorted(os.environ.items()):
print(f"{clave}={valor}")
# Establecer una variable (solo en el proceso actual)
os.environ["MI_VAR"] = "mi_valor"
print(os.environ["MI_VAR"])
2. python-dotenv: cargar archivos .env
pip install python-dotenv
Archivo .env (nunca lo subas a git — añádelo a .gitignore):
# .env
DATABASE_URL=postgresql://usuario:contraseña@localhost:5432/midb
SECRET_KEY=clave-secreta-super-segura
DEBUG=true
PORT=8000
ALLOWED_HOSTS=localhost,127.0.0.1
API_KEY=mi-api-key-privada
from dotenv import load_dotenv
import os
# Carga el archivo .env en os.environ
load_dotenv()
db_url = os.environ["DATABASE_URL"]
secret_key = os.environ["SECRET_KEY"]
debug = os.environ.get("DEBUG", "false") == "true"
port = int(os.environ.get("PORT", "8000"))
print(f"Conectando a: {db_url}")
Opciones avanzadas de load_dotenv
from dotenv import load_dotenv
from pathlib import Path
# Cargar archivo .env específico
load_dotenv(dotenv_path=Path("/ruta/a/.env.produccion"))
# No sobrescribir variables ya existentes en el sistema
load_dotenv(override=False)
# Verbose: ver qué variables se cargan
load_dotenv(verbose=True)
# Buscar .env subiendo por los directorios padre
from dotenv import find_dotenv
load_dotenv(find_dotenv())
3. Múltiples entornos: .env.development, .env.production
import os
from dotenv import load_dotenv
from pathlib import Path
ENTORNO = os.environ.get("APP_ENV", "development")
# Cargar primero el .env base, luego el específico del entorno
load_dotenv(".env")
load_dotenv(f".env.{ENTORNO}", override=True)
print(f"Entorno: {ENTORNO}")
print(f"DB: {os.environ.get('DATABASE_URL')}")
Estructura de archivos:
proyecto/
├── .env # Valores base (sin secretos)
├── .env.development # Valores locales (en .gitignore)
├── .env.production # Valores de producción (en .gitignore)
├── .env.example # Plantilla sin valores reales (en git)
└── app.py
4. Clase de configuración centralizada
import os
from dataclasses import dataclass, field
from dotenv import load_dotenv
load_dotenv()
@dataclass
class Config:
# Base de datos
database_url: str = field(default_factory=lambda: os.environ["DATABASE_URL"])
db_pool_size: int = field(default_factory=lambda: int(os.environ.get("DB_POOL_SIZE", "5")))
# Aplicación
secret_key: str = field(default_factory=lambda: os.environ["SECRET_KEY"])
debug: bool = field(default_factory=lambda: os.environ.get("DEBUG","false")=="true")
port: int = field(default_factory=lambda: int(os.environ.get("PORT","8000")))
allowed_hosts: list = field(default_factory=lambda: os.environ.get("ALLOWED_HOSTS","localhost").split(","))
# APIs externas
api_key: str = field(default_factory=lambda: os.environ.get("API_KEY", ""))
def __post_init__(self):
if not self.secret_key:
raise ValueError("SECRET_KEY es obligatoria")
if len(self.secret_key) < 32:
raise ValueError("SECRET_KEY debe tener al menos 32 caracteres")
# Singleton de configuración
config = Config()
print(f"Puerto: {config.port}")
print(f"Debug : {config.debug}")
print(f"Hosts : {config.allowed_hosts}")
5. Configuración con archivos TOML (Python 3.11+)
# config.toml
[app]
name = "MiApp"
version = "1.0.0"
debug = false
port = 8000
[database]
host = "localhost"
port = 5432
name = "midb"
pool_size = 5
[logging]
level = "INFO"
file = "app.log"
import tomllib # Python 3.11+
from pathlib import Path
with open("config.toml", "rb") as f:
config = tomllib.load(f)
print(config["app"]["name"])
print(config["database"]["host"])
print(config["logging"]["level"])
# Combinar con variables de entorno
import os
db_password = os.environ.get("DB_PASSWORD", "")
db_url = f"postgresql://{config['database']['host']}:{db_password}@{config['database']['host']}:{config['database']['port']}/{config['database']['name']}"
Para Python < 3.11: pip install tomli y import tomli as tomllib.
6. Configuración con archivos INI (configparser)
# config.ini
[DEFAULT]
debug = false
port = 8000
[development]
debug = true
database_url = sqlite:///dev.db
log_level = DEBUG
[production]
debug = false
database_url = postgresql://localhost/midb
log_level = WARNING
import configparser
import os
config = configparser.ConfigParser()
config.read("config.ini")
entorno = os.environ.get("APP_ENV", "development")
seccion = config[entorno]
debug = seccion.getboolean("debug")
port = seccion.getint("port")
db_url = seccion["database_url"]
log_level = seccion["log_level"]
print(f"Debug: {debug}, Puerto: {port}, DB: {db_url}")
7. Validación de configuración con pydantic
pip install pydantic pydantic-settings
from pydantic_settings import BaseSettings
from pydantic import AnyUrl, validator
class Settings(BaseSettings):
# Lee automáticamente desde .env y os.environ
database_url: AnyUrl
secret_key: str
debug: bool = False
port: int = 8000
allowed_hosts: list[str] = ["localhost"]
api_timeout: int = 30
@validator("secret_key")
def secret_key_min_length(cls, v):
if len(v) < 32:
raise ValueError("SECRET_KEY debe tener mínimo 32 caracteres")
return v
class Config:
env_file = ".env"
env_file_encoding = "utf-8"
settings = Settings()
print(settings.database_url)
print(settings.debug)
8. Ocultar secretos en logs
import os
import re
class SecretStr:
"""Wrapper que oculta el valor en representaciones de cadena."""
def __init__(self, value: str):
self._value = value
def get_secret_value(self) -> str:
return self._value
def __repr__(self) -> str:
return "**********"
def __str__(self) -> str:
return "**********"
# Uso
api_key = SecretStr(os.environ.get("API_KEY", ""))
print(f"API key: {api_key}") # API key: **********
print(api_key.get_secret_value()) # clave-real
# Función para enmascarar URLs con credenciales en logs
def mask_url(url: str) -> str:
return re.sub(r"://[^:]+:[^@]+@", "://***:***@", url)
db = "postgresql://usuario:password123@localhost/midb"
print(mask_url(db)) # postgresql://***:***@localhost/midb
9. Patrones recomendados por entorno
ENTORNO FUENTE DE CONFIG NOTAS
──────────────────────────────────────────────────────────────
desarrollo .env.development En .gitignore
CI/CD variables del pipeline GitHub Secrets, etc.
producción variables del sistema Nunca archivos .env
Docker docker-compose env_file Volumen externo
K8s ConfigMap + Secret Cifrado en etcd
# Patrón recomendado: falla rápido si falta algo obligatorio
import os
VARS_OBLIGATORIAS = ["DATABASE_URL", "SECRET_KEY", "API_KEY"]
def verificar_configuracion():
faltantes = [v for v in VARS_OBLIGATORIAS if not os.environ.get(v)]
if faltantes:
raise EnvironmentError(
f"Variables de entorno faltantes: {', '.join(faltantes)}\n"
f"Copia .env.example a .env y configura los valores."
)
verificar_configuracion()
Resumen
| Herramienta | Uso principal |
|---|---|
os.environ |
Leer variables del sistema |
python-dotenv |
Cargar archivos .env en local |
configparser |
Archivos .ini por sección/entorno |
tomllib |
Archivos .toml (Py 3.11+) |
pydantic-settings |
Validación de configuración tipada |
Conversiones relacionadas
Conversiones frecuentes del catálogo: