Configuración con variables de entorno en Python

Las variables de entorno son la forma de configurar un programa que corre en un contenedor. Python las lee en una línea; lo que hay que montar alrededor es la conversión y la validación.

Las tres formas de leer

salidaos.environ.get('NO_EXISTE')      None
os.environ.get('VACIA')          ''
'VACIA' in os.environ            True
os.getenv('NO_EXISTE', 'valor')  'valor'
os.environ['NO_EXISTE']          KeyError: 'NO_EXISTE'

Python sí distingue una variable vacía de una que no existe: la primera devuelve la cadena vacía y la segunda None. Aun así casi siempre conviene tratarlas igual, porque VAR= suele significar «no la he puesto».

Los corchetes revientan con KeyError, lo que está bien para lo que es verdaderamente obligatorio; get para lo demás.

Una función por tipo

def entero(k, omision):
    v = os.environ.get(k)
    if not v: return omision
    try: return int(v.strip())
    except ValueError: raise SystemExit(f"error: {k}={v!r} no es un entero")

VERDADEROS = {"1","true","t","yes","y","si","sí","on"}
def booleano(k, omision):
    v = os.environ.get(k)
    return omision if not v else v.strip().lower() in VERDADEROS

def obligatorio(k):
    v = os.environ.get(k)
    if not v: raise SystemExit(f"error: {k} es obligatoria y no está definida")
    return v
salidaConfig(host='localhost', puerto=9000, depurar=True, espera=datetime.timedelta(seconds=1, microseconds=500000), clave='********')

Y cuando algo está mal, el programa no arranca a medias:

y sale con código 1error: APP_CLAVE es obligatoria y no está definida
error: APP_PUERTO='ocho' no es un entero
raise SystemExit("mensaje") hace tres cosas de una vez: escribe el mensaje en la salida de errores, termina el programa y devuelve el código 1. Es más corto que print(..., file=sys.stderr) seguido de sys.exit(1), y al ser una excepción se puede capturar en las pruebas.

Ojo con el booleano: bool("False") es True, porque cualquier cadena no vacía lo es. Por eso hace falta la lista de valores aceptados.

Que la contraseña no se escape

class Secreto(str):
    __slots__ = ()
    def __repr__(self): return "'********'" if self else "'(vacío)'"
    def __str__(self):  return "********" if self else "(vacío)"

Mira la salida de arriba: el Config entero se imprimió y la clave salió como ********. Al heredar de str, el objeto sigue siendo una cadena y se puede usar donde haga falta, pero no aparece en ningún registro, volcado ni mensaje de error, porque todo eso pasa por repr o str.

Cambiarlo para un hijo, sin tocar el propio

import subprocess, sys

ent = os.environ | {"APP_PUERTO": "9999"}        # copia con un cambio
subprocess.run([sys.executable, "-c", "..."], env=ent)
salidael hijo ve 9999
y aquí sigue siendo 9000

Ese | entre diccionarios es de Python 3.9 y crea uno nuevo sin modificar el original, que es justo lo que quieres: os.environ es global al proceso y cambiarlo afecta a todo lo que venga después.

Para… Usa
Leer con valor por omisión os.getenv(k, omision)
Exigir que esté os.environ[k], o una función propia
Distinguir vacía de ausente k in os.environ
Convertir int(), y una lista para el booleano
Que no se filtre un secreto un tipo propio con __repr__
Pasárselo a un hijo env=os.environ | {...}
Leer un archivo .env python-dotenv, que no viene de serie

Todo el código se ejecutó con Python 3.14 en un contenedor limpio antes de publicar esta página; las salidas están copiadas de esa ejecución.