---
title: "0.2.4 Terminal, rutas, variables de entorno y .env"
description: "Los comandos básicos de la terminal, rutas absolutas y relativas con pathlib, variables de entorno y un lector de ficheros .env que no muestra las claves."
url: "https://pythonparatrading.com/curso-python-trading/nivel-0/terminal-rutas-y-variables-de-entorno/"
lang: "es"
image: "https://pythonparatrading.com/og/curso-python-trading/nivel-0/terminal-rutas-y-variables-de-entorno.png"
---

# 0.2.4 · Terminal básica, rutas, variables de entorno y ficheros .env

> Un sistema de trading se arranca desde una terminal, lee datos de una ruta y recibe sus claves por el entorno. Esta lección cubre los tres frentes y termina con un lector de `.env` escrito a mano que nunca enseña un secreto.

- Curso: [Nivel 0: Python desde cero y mentalidad de datos](https://pythonparatrading.com/curso-python-trading/nivel-0/)
- Módulo: 0.2 · Entorno de trabajo moderno
- Duración: 40 min
- Requisitos: [0.2.2 · Entornos virtuales y dependencias: uv init, uv add, pyproject.toml y lockfiles](https://pythonparatrading.com/curso-python-trading/nivel-0/entornos-virtuales-y-dependencias-con-uv/)

## Al terminar esta lección

- Moverte por carpetas, listar, crear y ver ficheros desde la terminal en Linux, macOS o Windows.
- Construir rutas con `pathlib` y explicar por qué una ruta relativa depende de la carpeta desde la que se ejecuta el programa.
- Leer variables de entorno con `os.environ.get`, con valores por defecto y conversión de tipos.
- Cargar la configuración de un fichero `.env` con la precedencia correcta (entorno sobre `.env`) sin mostrar nunca un secreto.

## La terminal en cinco minutos

La terminal es un programa que lee órdenes escritas y las ejecuta. Ya la has usado para instalar uv (lección 0.2.1) y
crear el proyecto (lección 0.2.2). En el trading sistemático es la herramienta diaria: arranca el backtest, lanza el
sistema en un servidor, lee sus logs y lo para.

Dentro de la terminal se ejecuta un **intérprete de órdenes** (*shell*): `bash` o `zsh` en Linux y macOS,
**PowerShell** en Windows. Cada uno tiene su sintaxis, pero los comandos básicos coinciden casi siempre, porque
PowerShell define alias con los nombres de Unix:

| Acción | Linux y macOS | Windows (PowerShell) |
|---|---|---|
| ¿En qué carpeta estoy? | `pwd` | `pwd` |
| Listar el contenido | `ls`, `ls -la` (también ocultos) | `ls`, `ls -Force` |
| Entrar en una carpeta / subir una | `cd datos`, `cd ..` | `cd datos`, `cd ..` |
| Crear una carpeta | `mkdir -p datos/brutos` | `mkdir datos\brutos` |
| Ver un fichero de texto | `cat .gitignore`, `head -5 sint.csv` | `cat .gitignore`, `Get-Content sint.csv -Head 5` |
| Copiar / borrar | `cp a b`, `rm a` | `cp a b`, `rm a` |
| Parar el programa en marcha | Ctrl+C | Ctrl+C |

Una sesión típica en el proyecto del curso:

```bash
cd ~/ppt-curso                 # ~ es tu carpeta personal
ls                             # pyproject.toml, src, datos...
head -3 datos/brutos/sint_diario.csv
uv run python -m ppt.backtest  # ejecuta un módulo de tu paquete
```

Tres hábitos ahorran horas: el **tabulador** completa nombres de ficheros y comandos; la **flecha arriba** recupera
los comandos anteriores; y **Ctrl+C** interrumpe el programa en marcha, que es la forma normal de parar un backtest
que tarda demasiado o un sistema en papel.

<aside class="callout" data-kind="aviso">

`rm` no tiene papelera: lo borrado no se recupera. Y `rm -r` borra carpetas enteras. Antes de borrar, `pwd` y `ls`
para confirmar dónde estás. Nunca copies en tu terminal un comando de un foro que no entiendas.

</aside>

## Rutas: absolutas y relativas

Una **ruta** dice dónde está un fichero. Es **absoluta** si empieza en la raíz del disco (`/home/ana/ppt-curso/…` en
Linux, `C:\Users\ana\ppt-curso\…` en Windows) y **relativa** si se interpreta desde la carpeta actual
(`datos/sint_diario.csv`). Python tiene un módulo para manejarlas sin pelearse con barras y separadores, `pathlib`:

```python
from pathlib import Path

ruta = Path("datos") / "brutos" / "sint_diario.csv"
print(ruta)
print(ruta.name, "|", ruta.stem, "|", ruta.suffix)
print(ruta.parent)
print(ruta.parts)
```
```output
datos/brutos/sint_diario.csv
sint_diario.csv | sint_diario | .csv
datos/brutos
('datos', 'brutos', 'sint_diario.csv')
```

El operador `/` une trozos de ruta con el separador del sistema: `/` en Linux y macOS, `\` en Windows. Por eso es mejor
que escribir rutas a mano como texto. En Windows, además, la barra invertida tiene una trampa: dentro de una cadena de
Python, `\n` es un salto de línea y `\t` un tabulador.

```python
from pathlib import PureWindowsPath

mal = "C:\nuevos\ticks.csv"          # \n y \t son caracteres
bien = r"C:\nuevos\ticks.csv"        # r"" = barras literales
print(len(mal), len(bien))
print(PureWindowsPath(bien).name)
print(PureWindowsPath(bien).as_posix())
```
```output
17 19
ticks.csv
C:/nuevos/ticks.csv
```

La primera cadena tiene dos caracteres menos: `\n` y `\t` se han convertido en un salto de línea y un tabulador, y el
fichero `ticks.csv` no aparecerá nunca. Con el prefijo `r` (cadena «cruda») o con `pathlib` el problema desaparece.
`PureWindowsPath` permite manejar rutas de Windows desde cualquier sistema; en tu código basta con `Path`.

El problema más frecuente con las rutas no es la barra, sino la **carpeta actual**. Una ruta relativa se resuelve
contra la carpeta desde la que se lanzó el programa, no contra la del fichero de código. El código del curso se
ejecuta en una carpeta que contiene los datos sueltos, así que:

```python
print(Path("sint_diario.csv").exists())
print(Path("datos/sint_diario.csv").exists())
print(Path("sint_diario.csv").is_absolute())
print(Path("sint_diario.csv").resolve().is_absolute())
```
```output
True
False
False
True
```

El mismo script, lanzado desde otra carpeta, por cron o por un servicio del servidor, no encontraría el fichero. La
solución es anclar las rutas a algo fijo. En un fichero `.py`, la variable `__file__` es la ruta del propio fichero;
a partir de ella se sube hasta la raíz del proyecto:

```python skip title="src/ppt/rutas.py"
from pathlib import Path

# src/ppt/rutas.py -> subir dos niveles = raíz del proyecto
RAIZ = Path(__file__).resolve().parents[2]
DATOS_BRUTOS = RAIZ / "datos" / "brutos"
DATOS_PROCESADOS = RAIZ / "datos" / "procesados"
```

El resto del código importa `DATOS_BRUTOS` y no vuelve a escribir una ruta. Si mañana los datos se mueven a otro
disco, se cambia una línea. Los ficheros CSV, Parquet y JSON con `pathlib` se ven en la lección 0.4.4.

## Variables de entorno

Cada programa recibe al arrancar un conjunto de pares `NOMBRE=valor`: sus **variables de entorno**. Algunas las pone
el sistema (`PATH`, la lista de carpetas donde la terminal busca los programas; por eso tras instalar uv había que
reabrir la terminal), y otras las pones tú. Son la forma estándar de pasar configuración a un programa sin tocar su
código: el modo de operación, el capital, la clave del bróker.

Se definen en la terminal antes de arrancar el programa:

```bash
export PPT_MODO=papel                  # Linux y macOS, para la sesión
$env:PPT_MODO = "papel"                # PowerShell, para la sesión
PPT_MODO=real uv run python sistema.py # Linux y macOS, solo esa ejecución
```

En Python se leen con `os.environ`, que funciona como un diccionario. Aquí la variable se crea desde Python para que
la celda sea reproducible; en tu sistema vendría de la terminal:

```python
import os

os.environ["PPT_MODO"] = "papel"      # en la vida real: export

print(os.environ.get("PPT_MODO"))
print(os.environ.get("PPT_CAPITAL"))             # no existe
print(os.environ.get("PPT_CAPITAL", "25000"))    # por defecto
```
```output
papel
None
25000
```

`os.environ["PPT_CAPITAL"]`, con corchetes, lanzaría un `KeyError` si la variable no existe; `.get()` devuelve `None`
o el valor por defecto. Usa corchetes para lo obligatorio (que falle pronto) y `.get()` con un valor por defecto
**seguro** para lo opcional. En trading, seguro significa conservador: si falta el modo, papel, no real.

Las variables de entorno son **siempre texto**. Un número hay que convertirlo, y un sí/no tiene trampa:

```python
os.environ["PPT_REAL"] = "false"
print(bool(os.environ["PPT_REAL"]))          # ¡cuidado!

def leer_bool(nombre: str, defecto: bool = False) -> bool:
    valor = os.environ.get(nombre)
    if valor is None:
        return defecto
    return valor.strip().lower() in {"1", "true", "si", "sí", "yes"}

print(leer_bool("PPT_REAL"))
capital = float(os.environ.get("PPT_CAPITAL", "25000"))
print(capital * 0.02)                        # riesgo máximo del 2 %
```
```output
True
False
500.0
```

`bool("false")` es `True`, porque cualquier cadena no vacía es verdadera. Un sistema que decide si opera en real con
`bool(os.environ[...])` operaría en real con `PPT_REAL=false`. `leer_bool` solo acepta un «sí» explícito; todo lo
demás, incluida una errata, cuenta como no.

## Ficheros .env

Definir diez variables a mano cada vez que abres la terminal no es práctico. La convención es guardarlas en un fichero
`.env` en la raíz del proyecto, una por línea, que el programa lee al arrancar. El `.env` es **de tu máquina**: tiene
tus claves y tu configuración, está en `.gitignore` desde el primer commit (lección 0.1.3) y no se comparte. Lo que sí
se versiona es un `.env.example` con los nombres y sin los valores.

Existen librerías para leerlo (`python-dotenv`, y Pydantic en la lección 0.4.2), pero el formato es tan simple que
merece la pena escribir el lector a mano para entender qué hacen. Primero, un `.env` de ejemplo con claves inventadas:

```python
Path(".env").write_text("""\
# Bróker (cuenta de papel)
BROKER_API_KEY=pk_demo_8f3a91c2e7b4
BROKER_API_SECRET="sk_demo_41d0c9e2a7f35b18"
export PPT_MODO=papel
PPT_CAPITAL=25000

PPT_RIESGO_MAX=0.02
""");
```

El lector: ignora líneas vacías y comentarios, admite el prefijo `export` (para que el mismo fichero sirva en `bash`),
separa por el primer `=` y quita las comillas que rodean el valor:

```python
def leer_env(ruta: Path) -> dict[str, str]:
    valores = {}
    for linea in ruta.read_text().splitlines():
        linea = linea.strip()
        if not linea or linea.startswith("#"):
            continue
        linea = linea.removeprefix("export ")
        clave, igual, valor = linea.partition("=")
        if not igual:
            continue                    # línea sin '=': se ignora
        valor = valor.strip()
        comillas = valor[:1] in ("'", '"')
        if comillas and len(valor) >= 2 and valor[-1] == valor[0]:
            valor = valor[1:-1]
        valores[clave.strip()] = valor
    return valores

env = leer_env(Path(".env"))
print(list(env))
```
```output
['BROKER_API_KEY', 'BROKER_API_SECRET', 'PPT_MODO', 'PPT_CAPITAL', 'PPT_RIESGO_MAX']
```

La celda imprime solo los nombres, no los valores. Es deliberado: un valor impreso acaba en la salida del notebook, en
un log o en una captura de pantalla. Cuando haga falta mostrar la configuración (por ejemplo, en el log de arranque
del sistema), los secretos se **enmascaran**:

```python
SENSIBLES = ("KEY", "SECRET", "TOKEN", "PASSWORD")

def enmascarar(clave: str, valor: str) -> str:
    if any(s in clave.upper() for s in SENSIBLES):
        return valor[:3] + "***" if len(valor) > 12 else "***"
    return valor

for clave, valor in env.items():
    print(f"{clave:<18} {enmascarar(clave, valor)}")
```
```output
BROKER_API_KEY     pk_***
BROKER_API_SECRET  sk_***
PPT_MODO           papel
PPT_CAPITAL        25000
PPT_RIESGO_MAX     0.02
```

Tres caracteres bastan para distinguir una clave de papel (`pk_`) de otra sin revelar nada útil.

## Quién gana: entorno, .env y valores por defecto

Un mismo programa se ejecuta en tu portátil, en un servidor y en los tests. Cada sitio necesita ajustar algo sin
tocar el código ni el `.env` de los demás. La convención, que siguen `python-dotenv` y `uv run --env-file`, es una
cadena de precedencia en la que cada capa sobrescribe a la anterior:

```mermaid
flowchart TB
  D[Por defecto] --> E[Fichero .env]
  E --> V[Entorno]
  V --> C[Configuración]
```

Los valores por defecto están en el código y son conservadores; el `.env` los ajusta para tu máquina; una variable de
entorno los cambia para una ejecución concreta. Con el lector anterior, la carga completa queda así, anotando de dónde
sale cada valor:

```python
DEFECTO = {"PPT_MODO": "papel", "PPT_CAPITAL": "10000",
           "PPT_RIESGO_MAX": "0.01", "PPT_LOG": "INFO"}

def cargar_config(ruta: Path) -> dict[str, tuple[str, str]]:
    config = {k: (v, "defecto") for k, v in DEFECTO.items()}
    for k, v in leer_env(ruta).items():
        config[k] = (v, ".env")
    for k in config:
        if k in os.environ:
            config[k] = (os.environ[k], "entorno")
    return config

os.environ["PPT_CAPITAL"] = "50000"   # export PPT_CAPITAL=50000
for clave, (valor, origen) in cargar_config(Path(".env")).items():
    print(f"{clave:<18} {enmascarar(clave, valor):<10} {origen}")
```
```output
PPT_MODO           papel      entorno
PPT_CAPITAL        50000      entorno
PPT_RIESGO_MAX     0.02       .env
PPT_LOG            INFO       defecto
BROKER_API_KEY     pk_***     .env
BROKER_API_SECRET  sk_***     .env
```

Cada valor lleva su origen: cuando un sistema hace algo inesperado, saber de dónde salió el capital o el modo es la
mitad del diagnóstico. `PPT_MODO` sale del entorno porque una celda anterior la definió con `os.environ`; es el
mismo efecto que tendría un `export` olvidado en la terminal.

En tus scripts no hace falta llamar a un lector propio: uv carga el `.env` al arrancar el programa si se lo pides.

```bash
uv run --env-file .env python -m ppt.backtest
uv run --env-file .env --env-file .env.servidor python -m ppt.sistema
```

Las variables del fichero llegan a `os.environ` y, como en `cargar_config`, una variable que ya existía en el entorno
no se sobrescribe. Con varios ficheros, cada uno sobrescribe al anterior: un `.env` común y otro con lo propio del
servidor. Lo que uv no hace es validar los valores ni enmascarar los secretos al mostrarlos; eso sigue siendo cosa de
tu código, como en el segundo ejercicio.

<aside class="callout" data-kind="riesgo">

Una clave de API de un bróker es dinero. Reglas sin excepciones: va en `.env` (o en el gestor de secretos del
servidor), nunca en el código; `.env` está en `.gitignore` antes del primer commit; no se imprime, ni en logs ni en
notebooks ni en un prompt de IA; mientras aprendes, solo claves de **cuenta de papel** y, si el bróker lo permite,
sin permiso de retirada de fondos. Si una clave sale de tu máquina por error, se revoca y se crea otra: borrar el
fichero no basta (lección 0.2.5).

</aside>

## Ejercicios

1. En la terminal, sin usar el explorador de archivos: crea dentro de `ppt-curso` las carpetas `datos/brutos` y
   `datos/procesados`, copia en `datos/brutos` el fichero `sint_diario.csv` de los recursos y muestra sus tres primeras
   líneas.

**Solución.**

En Linux y macOS: `cd ~/ppt-curso`, `mkdir -p datos/brutos datos/procesados`, `cp ~/Descargas/sint_diario.csv
datos/brutos/` (la carpeta de descargas depende de tu sistema) y `head -3 datos/brutos/sint_diario.csv`. En
PowerShell: `mkdir datos\brutos`, `mkdir datos\procesados`, `cp $HOME\Downloads\sint_diario.csv datos\brutos\` y
`Get-Content datos\brutos\sint_diario.csv -Head 3`. La primera línea es la cabecera:
`fecha,apertura,maximo,minimo,cierre,volumen`.

2. Amplía `cargar_config` para que falle con un mensaje claro si falta una variable obligatoria (por ejemplo,
   `BROKER_API_KEY`) y para que convierta `PPT_CAPITAL` y `PPT_RIESGO_MAX` a `float`, comprobando que el riesgo está
   entre 0 y 0,05.

**Solución.**

```python
OBLIGATORIAS = ("BROKER_API_KEY", "BROKER_API_SECRET")

def config_validada(ruta: Path) -> dict[str, object]:
    crudo = {k: v for k, (v, _) in cargar_config(ruta).items()}
    faltan = [k for k in OBLIGATORIAS if not crudo.get(k)]
    if faltan:
        raise RuntimeError(f"Faltan variables: {', '.join(faltan)}")
    riesgo = float(crudo["PPT_RIESGO_MAX"])
    if not 0 < riesgo <= 0.05:
        raise ValueError(f"PPT_RIESGO_MAX fuera de rango: {riesgo}")
    return crudo | {"PPT_CAPITAL": float(crudo["PPT_CAPITAL"]),
                    "PPT_RIESGO_MAX": riesgo}

cfg = config_validada(Path(".env"))
print(cfg["PPT_CAPITAL"], cfg["PPT_RIESGO_MAX"])
```
```output
50000.0 0.02
```

El mensaje nombra las variables que faltan, pero nunca muestra valores. Fallar al arrancar es mucho mejor que
descubrir a mitad de sesión que el capital era una cadena o que el riesgo era del 20 %.

3. Escribe el `.env.example` que acompañaría al `.env` de la lección. ¿Qué diferencia hay entre los dos y cuál va a
   Git?

**Solución.**

```dotenv title=".env.example"
# Copia este fichero a .env y rellena los valores
BROKER_API_KEY=
BROKER_API_SECRET=
PPT_MODO=papel
PPT_CAPITAL=25000
PPT_RIESGO_MAX=0.02
```

El `.env.example` tiene los mismos nombres, con los secretos vacíos y valores de ejemplo para lo que no es secreto.
Va a Git: documenta qué necesita el programa. El `.env`, con las claves de verdad, no se versiona nunca.

## Autoevaluación

1. Tu script lee `datos/sint_diario.csv` y funciona cuando lo lanzas desde la carpeta del proyecto. Programado con cron, falla con `FileNotFoundError`. ¿Por qué?

   - A. Porque cron no tiene permisos para leer CSV
   - B. Porque la ruta es relativa y se resuelve contra la carpeta actual, que con cron no es la del proyecto
   - C. Porque pandas no funciona fuera de la terminal
   - D. Porque el fichero se borró

   *Respuesta B: Porque la ruta es relativa y se resuelve contra la carpeta actual, que con cron no es la del proyecto.* Una ruta relativa se interpreta desde la carpeta de trabajo del proceso. Cron, un servicio o un editor pueden arrancar el programa desde otra carpeta. La solución es construir la ruta a partir de la ubicación del propio fichero de código o de una raíz conocida.

2. ¿Qué devuelve `bool(os.environ.get("PPT_REAL", "false"))` si la variable no existe?

   - A. `False`
   - B. `True`, porque `"false"` es una cadena no vacía
   - C. `None`
   - D. Un error

   *Respuesta B: `True`, porque `"false"` es una cadena no vacía.* Las variables de entorno son siempre texto, y cualquier cadena no vacía es verdadera. Para leer un sí/no hay que comparar el texto con valores concretos, como hace `leer_bool` en la lección. Un error así puede poner un sistema a operar en real.

3. La variable `PPT_CAPITAL` vale `10000` en el entorno y `25000` en el `.env`. Con la precedencia de la lección, ¿qué capital usa el sistema?

   - A. 25000, porque el `.env` es del proyecto
   - B. 10000, porque el entorno tiene prioridad sobre el `.env`
   - C. La suma, 35000
   - D. Ninguno: el programa falla por el conflicto

   *Respuesta B: 10000, porque el entorno tiene prioridad sobre el `.env`.* El `.env` da valores por defecto para tu máquina; el entorno permite cambiarlos en una ejecución concreta (un servidor, un test) sin tocar ficheros. Es la misma regla que siguen uv con `--env-file` y la librería `python-dotenv`.

4. Has subido por error un `.env` con la clave de tu bróker a un repositorio público y lo has borrado con otro commit cinco minutos después. ¿Qué haces?

   - A. Nada: ya está borrado
   - B. Hacer el repositorio privado
   - C. Revocar la clave en el bróker y crear otra; la antigua sigue en el historial y puede haberse copiado ya
   - D. Cambiar el nombre del fichero

   *Respuesta C: Revocar la clave en el bróker y crear otra; la antigua sigue en el historial y puede haberse copiado ya.* Hay programas que rastrean de forma continua los repositorios públicos en busca de claves, y el commit sigue en el historial. Una clave que ha estado en Git se da por comprometida: se revoca, se crea otra y se revisa la actividad de la cuenta.

## Ideas clave

- La terminal es la forma de arrancar, parar (Ctrl+C) y automatizar programas. Pocos comandos bastan: `pwd`, `ls`, `cd`, `mkdir`, `cat`.
- Una ruta relativa depende de la carpeta actual. Construye las rutas con `pathlib` a partir de una raíz conocida.
- Las variables de entorno son siempre texto: convierte números y sí/no de forma explícita y da un valor por defecto seguro.
- Precedencia: valores por defecto, después `.env`, después el entorno. El entorno gana.
- Los secretos van en `.env`, que está en `.gitignore`; nunca se imprimen, ni en logs ni en notebooks.

## Recursos

### Documentación

- [Documentación de Python: pathlib](https://docs.python.org/es/3/library/pathlib.html) · docs.python.org
- [Documentación de Python: os.environ](https://docs.python.org/es/3/library/os.html#os.environ) · docs.python.org
- [The Twelve-Factor App: configuración en el entorno](https://12factor.net/es/config) · 12factor.net
- [PowerShell: variables de entorno (about_Environment_Variables)](https://learn.microsoft.com/es-es/powershell/module/microsoft.powershell.core/about/about_environment_variables) · learn.microsoft.com
- [uv: ejecutar comandos en un proyecto (incluye --env-file)](https://docs.astral.sh/uv/concepts/projects/run/) · docs.astral.sh

Lección anterior: [0.2.3 · Editores (VS Code, Cursor, PyCharm) y notebooks (Jupyter, JupyterLab, marimo)](https://pythonparatrading.com/curso-python-trading/nivel-0/editores-y-notebooks-jupyter-marimo/)

Lección siguiente: [0.2.5 · Git y GitHub: commits, ramas, pull requests y .gitignore](https://pythonparatrading.com/curso-python-trading/nivel-0/git-y-github-para-trading/)
