Saltar al contenido
🐍 Curso de Python
/

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.

⏱ 17 min de lectura

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:

bash
echo $HOME
Salida en pantalla
/Users/erwin

Desde Python se accede a ellas con el módulo os.

Las tres formas de leerlas#

entorno.py
import os

print(os.environ["USER"])
print(os.environ.get("EDITOR", "nano"))
print(os.getenv("API_TOKEN"))
print(os.getenv("NIVEL_REGISTRO", "INFO"))
bash
python3 entorno.py
Salida en pantalla
erwin
nano
None
INFO

os.environ es, a todos los efectos, un diccionario, así que se comporta como ya sabes:

FormaSi 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.

tipos_entorno.py
import os

puerto = os.getenv("PUERTO", "8080")
print(puerto, type(puerto))
print(int(puerto) + 1)

depurar = os.getenv("DEPURAR", "")
print(repr(depurar), bool(depurar))
bash
PUERTO=9000 DEPURAR=false python3 tipos_entorno.py
Salida en pantalla
9000 <class 'str'>
9001
'false' True

Mira 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:

booleanos.py
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')}")
Salida en pantalla
     '1' -> True
  'true' -> True
  'TRUE' -> True
    'sí' -> True
    'on' -> True
     '0' -> False
 'false' -> False
    'no' -> False
      '' -> False
(sin definir) -> False

Para los números, la conversión también hay que protegerla, porque en el entorno puede haber cualquier cosa:

python
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:

bash
PUERTO=9000 NIVEL_REGISTRO=DEBUG python3 entorno.py

Es 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í:

bash
export API_TOKEN="sk-de-mentira-1234567890"
python3 entorno.py
unset API_TOKEN

unset 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é quieresPowerShellConsola cmd
Ver una variableecho $env:PUERTOecho %PUERTO%
Definirla para la sesión$env:PUERTO = "9000"set PUERTO=9000
Definirla para siempresetx PUERTO 9000setx PUERTO 9000
Borrarla de la sesiónRemove-Item Env:PUERTOset 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:

VariableQué contienePara qué te sirve
PATHLas carpetas donde el sistema busca los programasPor qué python3 funciona desde cualquier sitio
HOMELa carpeta personal del usuarioDónde guardar la configuración. En Windows es USERPROFILE
USEREl nombre del usuarioSaludar, registrar quién ejecutó algo. En Windows es USERNAME
EDITOREl editor de texto preferidoAbrir un fichero para que lo edite el usuario, como hace git commit
LANGEl idioma y la codificación del sistemaDecidir en qué idioma hablar
NO_COLORSi está definida, con cualquier valor, no uses colorRespetarla en tu salida con ANSI
habituales.py
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)
bash
EDITOR=vim NO_COLOR=1 python3 habituales.py
Salida en pantalla
Usuario: erwin
Carpeta personal: /Users/erwin
Editor preferido: vim
Sin color: True

Fí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:

.env
# 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=false

El formato es tan simple que leerlo no necesita ninguna biblioteca:

cargar_env.py
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)"))
bash
python3 cargar_env.py
Salida en pantalla
Cargadas 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:

bash
NIVEL_REGISTRO=WARNING python3 cargar_env.py
Salida en pantalla
Cargadas 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:

.gitignore
.env
__pycache__/
*.pyc

Y para que quien clone el proyecto sepa qué variables necesita, se sube un .env.ejemplo sin valores reales, que sí entra en git:

.env.ejemplo
API_TOKEN=
BASE_DATOS_URL=postgresql://localhost:5432/tienda
NIVEL_REGISTRO=INFO
ENVIAR_CORREOS=false