Módulo 2 · Lección 16 de 28
Variables de entorno
Cómo configurar un programa desde fuera, sin tocar su código, y dónde guardar las claves que no pueden acabar en git.
Tu herramienta funciona. Ahora hay que ponerla en un servidor, donde la base de datos está en otra dirección, el nivel de detalle del registro tiene que ser menor y hace falta una clave de acceso a un servicio externo. Con lo que sabes hasta ahora tienes dos salidas, y las dos son malas: editar el código para cada sitio donde se ejecute, o pasar la clave como argumento de la línea de comandos, donde queda guardada en el historial de la terminal y visible para cualquiera que liste los procesos de la máquina.
La solución que usa todo el mundo son las variables de entorno: una lista de pares nombre-valor que el sistema operativo le entrega a cada programa cuando arranca. El mismo código, sin tocar una línea, se comporta distinto según lo que encuentre ahí.
Qué son exactamente#
Cada proceso hereda del que lo lanzó un diccionario de cadenas. Cuando abres una terminal, ese diccionario ya viene con unas cuantas variables puestas por el sistema, y todo lo que lances desde ahí las recibirá.
En macOS y Linux las ves todas con env, y una concreta con echo:
echo $HOME/Users/erwinDesde Python se accede a ellas con el módulo os.
Las tres formas de leerlas#
import os
print(os.environ["USER"])
print(os.environ.get("EDITOR", "nano"))
print(os.getenv("API_TOKEN"))
print(os.getenv("NIVEL_REGISTRO", "INFO"))python3 entorno.pyerwin
nano
None
INFOos.environ es, a todos los efectos, un diccionario, así que se comporta como ya sabes:
| Forma | Si la variable no existe |
|---|---|
os.environ["CLAVE"] | Lanza KeyError |
os.environ.get("CLAVE") | Devuelve None |
os.environ.get("CLAVE", "valor") | Devuelve "valor" |
os.getenv("CLAVE") | Devuelve None |
os.getenv("CLAVE", "valor") | Devuelve "valor" |
os.getenv y os.environ.get hacen literalmente lo mismo; la primera es un atajo de la segunda. Usa la que te resulte más legible, pero sé coherente dentro de un mismo programa.
Siempre son cadenas#
Esto no tiene excepciones: da igual que escribas PUERTO=9000, lo que llega a Python es el texto "9000". La conversión es cosa tuya.
import os
puerto = os.getenv("PUERTO", "8080")
print(puerto, type(puerto))
print(int(puerto) + 1)
depurar = os.getenv("DEPURAR", "")
print(repr(depurar), bool(depurar))PUERTO=9000 DEPURAR=false python3 tipos_entorno.py9000 <class 'str'>
9001
'false' TrueMira la última línea. Has escrito DEPURAR=false, con toda la intención de desactivar algo, y bool() te ha devuelto True. No es un fallo de Python: "false" es una cadena con cinco caracteres dentro, y en Python cualquier cadena no vacía es verdadera. Si usaras bool(os.getenv("DEPURAR")) directamente, tu programa estaría en modo depuración exactamente cuando pediste que no lo estuviera.
La forma correcta es decidir tú qué textos cuentan como verdadero:
import os
VERDADEROS = {"1", "true", "yes", "si", "sí", "on"}
def variable_activada(nombre: str, por_defecto: bool = False) -> bool:
"""Interpreta una variable de entorno como un booleano."""
valor = os.getenv(nombre)
if valor is None:
return por_defecto
return valor.strip().lower() in VERDADEROS
for prueba in ["1", "true", "TRUE", "sí", "on", "0", "false", "no", ""]:
os.environ["DEPURAR"] = prueba
print(f"{prueba!r:>8} -> {variable_activada('DEPURAR')}")
del os.environ["DEPURAR"]
print(f"{'(sin definir)':>13} -> {variable_activada('DEPURAR')}") '1' -> True
'true' -> True
'TRUE' -> True
'sí' -> True
'on' -> True
'0' -> False
'false' -> False
'no' -> False
'' -> False
(sin definir) -> FalsePara los números, la conversión también hay que protegerla, porque en el entorno puede haber cualquier cosa:
def variable_entera(nombre: str, por_defecto: int) -> int:
"""Lee una variable de entorno como entero, o devuelve el valor por defecto."""
valor = os.getenv(nombre)
if valor is None:
return por_defecto
try:
return int(valor)
except ValueError:
raise SystemExit(f"{nombre} debe ser un número entero, no {valor!r}")Definirlas desde la terminal#
Para una sola ejecución#
En macOS y Linux basta con ponerlas delante del comando, separadas por espacios. Solo existen durante esa ejecución y desaparecen después:
PUERTO=9000 NIVEL_REGISTRO=DEBUG python3 entorno.pyEs la forma más limpia de probar una configuración distinta sin ensuciar nada.
Para toda la sesión de la terminal#
Con export la variable queda puesta hasta que cierres esa ventana, y la heredan todos los programas que lances desde ahí:
export API_TOKEN="sk-de-mentira-1234567890"
python3 entorno.py
unset API_TOKENunset la borra. Si abres otra terminal, esta no tendrá nada de eso: cada ventana es un entorno independiente.
Para siempre#
Para que una variable esté puesta cada vez que abras una terminal, se añade la línea export al fichero de arranque de tu intérprete de órdenes: ~/.zshrc en macOS (y en Linux si usas zsh) o ~/.bashrc en la mayoría de Linux. El cambio afecta a las terminales que abras después, no a la que tienes abierta.
En Windows#
Cambia la sintaxis, no la idea:
| Qué quieres | PowerShell | Consola cmd |
|---|---|---|
| Ver una variable | echo $env:PUERTO | echo %PUERTO% |
| Definirla para la sesión | $env:PUERTO = "9000" | set PUERTO=9000 |
| Definirla para siempre | setx PUERTO 9000 | setx PUERTO 9000 |
| Borrarla de la sesión | Remove-Item Env:PUERTO | set PUERTO= |
En PowerShell no existe el atajo de ponerla delante del comando: hay que definirla y después lanzar el programa, en dos instrucciones o separadas por punto y coma. Y setx no afecta a la ventana actual, solo a las que abras a partir de entonces.
Las variables que ya existen#
Estas están puestas por el sistema y conviene conocerlas, porque tu programa puede aprovecharlas:
| Variable | Qué contiene | Para qué te sirve |
|---|---|---|
PATH | Las carpetas donde el sistema busca los programas | Por qué python3 funciona desde cualquier sitio |
HOME | La carpeta personal del usuario | Dónde guardar la configuración. En Windows es USERPROFILE |
USER | El nombre del usuario | Saludar, registrar quién ejecutó algo. En Windows es USERNAME |
EDITOR | El editor de texto preferido | Abrir un fichero para que lo edite el usuario, como hace git commit |
LANG | El idioma y la codificación del sistema | Decidir en qué idioma hablar |
NO_COLOR | Si está definida, con cualquier valor, no uses color | Respetarla en tu salida con ANSI |
import os
print("Usuario:", os.getenv("USER") or os.getenv("USERNAME"))
print("Carpeta personal:", os.path.expanduser("~"))
print("Editor preferido:", os.getenv("EDITOR", "no definido"))
print("Sin color:", "NO_COLOR" in os.environ)EDITOR=vim NO_COLOR=1 python3 habituales.pyUsuario: erwin
Carpeta personal: /Users/erwin
Editor preferido: vim
Sin color: TrueFíjate en dos detalles. os.getenv("USER") or os.getenv("USERNAME") cubre los tres sistemas de una tacada, aprovechando que or devuelve el primer valor que no sea falso. Y para la carpeta personal es mejor os.path.expanduser("~") que leer HOME, porque funciona igual en Windows sin tener que saber cómo se llama la variable allí.
Ficheros .env#
Definir seis variables a mano cada vez que abres una terminal es insufrible. La costumbre, nacida en el desarrollo web y extendida a todo, es guardarlas en un fichero llamado .env en la raíz del proyecto:
# Credenciales de desarrollo. Este fichero NO se sube a git.
API_TOKEN=sk-de-mentira-1234567890
BASE_DATOS_URL=postgresql://localhost:5432/tienda
NIVEL_REGISTRO=DEBUG
ENVIAR_CORREOS=falseEl formato es tan simple que leerlo no necesita ninguna biblioteca:
import os
from pathlib import Path
def cargar_env(ruta: str | Path = ".env") -> int:
"""Carga en os.environ las variables de un fichero .env. Devuelve cuántas."""
fichero = Path(ruta)
if not fichero.exists():
return 0
cargadas = 0
for linea in fichero.read_text(encoding="utf-8").splitlines():
linea = linea.strip()
if not linea or linea.startswith("#") or "=" not in linea:
continue
clave, _, valor = linea.partition("=")
clave = clave.strip()
valor = valor.strip().strip("\"'")
# Lo que ya venga del entorno real manda sobre el fichero.
os.environ.setdefault(clave, valor)
cargadas += 1
return cargadas
cuantas = cargar_env()
print(f"Cargadas {cuantas} variables de .env")
print("NIVEL_REGISTRO:", os.getenv("NIVEL_REGISTRO"))
print("ENVIAR_CORREOS:", os.getenv("ENVIAR_CORREOS"))
print("SIN_DEFINIR:", os.getenv("SIN_DEFINIR", "(valor por defecto)"))python3 cargar_env.pyCargadas 4 variables de .env
NIVEL_REGISTRO: DEBUG
ENVIAR_CORREOS: false
SIN_DEFINIR: (valor por defecto)Escribir en os.environ es perfectamente legal, y afecta a este proceso y a los que él lance, nunca a la terminal desde la que arrancaste: cuando el programa termina, no queda rastro. La clave del diseño está en setdefault, que solo escribe si la clave no existía. Así, una variable puesta de verdad en el entorno gana sobre lo que diga el fichero, y puedes saltarte el .env puntualmente:
NIVEL_REGISTRO=WARNING python3 cargar_env.pyCargadas 4 variables de .env
NIVEL_REGISTRO: WARNING
ENVIAR_CORREOS: false
SIN_DEFINIR: (valor por defecto)La regla de oro#
Las credenciales, las claves de API, las contraseñas de base de datos y los tokens no se escriben nunca en el código. Dan igual las buenas intenciones: en cuanto están en un fichero .py, están en el repositorio, y en cuanto están en el repositorio, están en el historial de git para siempre, aunque después los borres.
Por eso el .env es lo primero que se añade a .gitignore:
.env
__pycache__/
*.pycY para que quien clone el proyecto sepa qué variables necesita, se sube un .env.ejemplo sin valores reales, que sí entra en git:
API_TOKEN=
BASE_DATOS_URL=postgresql://localhost:5432/tienda
NIVEL_REGISTRO=INFO
ENVIAR_CORREOS=false