pathlib, configuración JSON/YAML y variables de entorno
Rutas que funcionan desde cualquier carpeta con pathlib, configuración JSON y YAML anidada, .env y variables de entorno, y un cargador de configuración que se detiene al inicio cuando un valor es incorrecto.
¿Cómo quieres aprender?
El problema que vamos a resolver
Un proyecto de bot de soporte tiene un script pequeño, scripts/show_config.py, que lee config/app.json y muestra el nombre del modelo y la temperatura. La persona autora siempre se queda en la raíz del proyecto y ejecuta python scripts/show_config.py, y funciona. Un día, un compañero entró en la carpeta scripts/, ejecutó el mismo script y recibió un FileNotFoundError — con el archivo exactamente donde siempre había estado.
Esa misma semana pasaron dos cosas más. El servidor tenía APP_DEBUG=false, pero los registros mostraban el modo de depuración activado. Y alguien escribió temperature: 0 en la configuración para que el modelo diera siempre la misma respuesta, pero el programa se ejecutó con 0.7. Ninguna de estas cosas lanzó un error.
Ninguno de los tres es un problema de la lógica del código. El problema está en cómo el programa lee el mundo exterior: el sistema de archivos, los archivos de configuración y las variables de entorno. En este capítulo hacemos esa lectura fiable: rutas con pathlib, configuración anidada con JSON y YAML, y secretos con .env. Cada una con validación, para que una configuración incorrecta se detenga al inicio en vez de ejecutarse en silencio.
Al terminar este capítulo podrás
- Construir rutas que funcionen en cualquier sistema operativo con
pathlib.Path, usando/en lugar de unir cadenas conos.path - Encontrar archivos desde la ubicación del propio script, sin importar dónde esté la terminal
- Leer configuración JSON y YAML anidada, y reconocer las formas en que
yaml.safe_loadcambia los tipos en silencio - Leer valores de variables de entorno y de un archivo
.env, convertirlos al tipo correcto y validarlos - Escribir un cargador de configuración que se detenga con un mensaje claro cuando un valor es incorrecto o falta
Requisitos previos: pyproject.toml, lockfiles y la estructura src/, sobre todo uv add y uv run.
pathlib: una ruta es un objeto, no una cadena
En el código de Python más antiguo, una ruta era una cadena, pegada con + "/" + o con os.path.join. La clase Path del módulo pathlib trata la ruta como un objeto cuyas partes puedes consultar:
from pathlib import Path
config_file = Path("config") / "models" / "chat.yaml"
print(config_file.as_posix())
print(config_file.name)
print(config_file.stem)
print(config_file.suffix)
print(config_file.parent.as_posix())
print(config_file.with_suffix(".json").as_posix())
print(config_file.parts)config/models/chat.yaml
chat.yaml
chat
.yaml
config/models
config/models/chat.json
('config', 'models', 'chat.yaml')Aquí / no es una división: usado con un Path, une partes de la ruta y elige el separador correcto para el sistema operativo. La salida se imprime con .as_posix() para que se vea igual en cualquier máquina; un print(config_file) normal muestra config\models\chat.yaml en Windows y config/models/chat.yaml en macOS y Linux.
Pegar cadenas muestra la diferencia:
from pathlib import Path
base = "config"
print(base + "/" + "app.json")
print(Path(base) / "app.json")
print(repr(Path(base) / "app.json"))En Windows, esto imprime:
config/app.json
config\app.json
WindowsPath('config/app.json')La primera línea es una cadena construida a mano. Windows suele aceptar /, pero en cuanto pasas esas rutas a otras herramientas o las comparas como cadenas, mezclar dos tipos de separador causa confusión. Path construye cada ruta según las reglas de su propio sistema. En macOS o Linux, la segunda línea muestra config/app.json y la tercera PosixPath('config/app.json').
/solo funciona cuando al menos uno de los lados es unPath. En"config" / "app.json"los dos lados son cadenas, así que Python lo lee como división y lanza unTypeError. Haz que la parte de la izquierda sea unPathy el resto puede seguir siendo cadena.
Algunos métodos más para el trabajo diario, que usan los ejemplos siguientes:
path.exists(),path.is_file(),path.is_dir(): si la ruta existe y qué tipo de cosa espath.read_text(encoding="utf-8")ypath.write_text(text, encoding="utf-8"): leer o escribir un archivo completo sinopenpath.mkdir(parents=True, exist_ok=True): crear una carpeta, incluidas las intermedias, sin error si ya existefolder.glob("*.txt"): todos los archivos de una carpeta que coinciden con un patrón;rglobbusca también en las subcarpetaspath.resolve(): convierte una ruta relativa en absoluta
Escribe encoding="utf-8" siempre. En Windows, la codificación por defecto de Python a menudo no es UTF-8, así que un archivo de prompt con texto en bengalí o con un emoji se lee bien en un Mac y produce caracteres ilegibles o un UnicodeDecodeError en Windows.
¿Relativo a qué?
El script del inicio del capítulo tiene este aspecto:
from pathlib import Path
config_path = Path("config/app.json")
print(config_path.read_text(encoding="utf-8"))Desde la raíz del proyecto, python scripts/show_config.py imprime la configuración. Pero entra en scripts/ y ejecuta python show_config.py:
Traceback (most recent call last):
File "show_config.py", line 4, in <module>
print(config_path.read_text(encoding="utf-8"))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "...\Lib\pathlib.py", line 1028, in read_text
with self.open(mode='r', encoding=encoding, errors=errors) as f:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "...\Lib\pathlib.py", line 1014, in open
return io.open(self, mode, buffering, encoding, errors, newline)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
FileNotFoundError: [Errno 2] No such file or directory: 'config\\app.json'(En tu máquina el traceback muestra rutas completas; aquí se han acortado.) Una ruta relativa siempre se busca desde el directorio de trabajo actual, es decir, donde esté la terminal, y no desde donde vive el archivo del script. No hay ningún config/app.json dentro de scripts/, así que el archivo no se encontró.
En producción pasa aún más: no siempre sabes desde qué carpeta arrancará tu programa un cron job, un gestor de servicios o un contenedor. La solución es atar la ruta a la ubicación del propio script. Dentro de cada módulo, __file__ contiene la ruta de ese archivo:
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parent.parent
config_path = PROJECT_ROOT / "config" / "app.json"
print(config_path.read_text(encoding="utf-8")){"model": "chat-small", "temperature": 0.2}Ahora imprime la misma línea tanto si lo ejecutas desde la raíz del proyecto, como desde scripts/, o desde cualquier otro lugar del disco con una ruta completa. Paso a paso: Path(__file__) es show_config.py; .resolve() la hace absoluta; el primer .parent es scripts/; el segundo .parent es la raíz del proyecto.
No quites.resolve(). En algunas situaciones__file__es una ruta relativa, y entonces.parent.parentvuelve a depender de la carpeta actual: exactamente el problema que intentabas evitar.
JSON: configuración anidada y la trampa del cero
La configuración en proyectos de IA casi siempre está anidada: ajustes de la aplicación, del modelo y de la recuperación de información en grupos separados. json.loads convierte una cadena JSON en diccionarios anidados (desde un archivo, json.loads(path.read_text(encoding="utf-8"))):
import json
raw = """
{
"app": {"name": "support-bot", "debug": false},
"llm": {"model": "chat-small", "temperature": 0, "max_tokens": 512},
"retrieval": {"top_k": 4}
}
"""
config = json.loads(raw)
print(config["llm"]["model"])
print(config["retrieval"]["top_k"])
print(type(config["app"]["debug"]))
temperature = config["llm"].get("temperature") or 0.7
print(temperature)
temperature = config["llm"].get("temperature", 0.7)
print(temperature)chat-small
4
<class 'bool'>
0.7
0Los false de JSON se convierten en False de Python, y los números siguen siendo números: JSON conserva los tipos. Pero fíjate en las dos últimas líneas. La configuración dice claramente "temperature": 0, pero el primer enfoque dio 0.7.
x or default significa «x si x es verdadero; si no, default». Y en Python, 0, 0.0, "", [] y {} son valores «falsy». Así que or no puede distinguir entre «no hay valor» y «hay un valor, y es cero». Una temperatura de 0 hace que el modelo dé siempre su respuesta más probable, que es lo que quieres para tests y evaluaciones; or la convierte en silencio en 0.7, y lo único que ves son respuestas que cambian de una ejecución a otra.
Usa.get(key, default)para los valores por defecto: solo usa el valor por defecto cuando la clave de verdad no existe.orsirve solo cuando de verdad quieres descartar también los ceros y los valores vacíos.
YAML: fácil para las personas, pero adivina los tipos
Muchos equipos escriben la configuración en YAML, porque permite comentarios y tiene menos corchetes y comillas. Python no trae un lector de YAML; añádelo al proyecto:
uv add pyyamlEl paquete se llama pyyaml, pero se importa como import yaml. Ahora lee una configuración:
import yaml
raw = """
llm:
model: chat-small
temperature: 0.2
features:
streaming: yes
cache: off
python_version: 3.10
api_version: "3.10"
"""
config = yaml.safe_load(raw)
print(config["llm"])
print(config["features"])
print(config["python_version"], type(config["python_version"]).__name__)
print(config["api_version"], type(config["api_version"]).__name__){'model': 'chat-small', 'temperature': 0.2}
{'streaming': True, 'cache': False}
3.1 float
3.10 strA diferencia de JSON, YAML no te obliga a escribir los tipos: el parser los adivina. La mayoría de las veces la suposición es correcta, pero en dos casos se equivoca en silencio. yes, no, on y off se convierten en booleanos: el archivo decía cache: off y tú obtienes False. Y 3.10 se lee como un número decimal cuyo valor es 3.1; como número de versión, eso es algo completamente distinto. El "3.10" entre comillas sigue siendo una cadena.
La regla es sencilla: cualquier valor que parezca un número o un sí/no pero que en realidad es texto (números de versión, IDs, códigos postales) va entre comillas en YAML.
Y usa siempre yaml.safe_load. El yaml.load(text) que ves en tutoriales antiguos ahora falla directamente con TypeError: load() missing 1 required positional argument: 'Loader'. Ese cambio es deliberado: algunos cargadores pueden construir objetos arbitrarios de Python a partir de instrucciones dentro del archivo YAML, así que leer un archivo YAML no confiable significa ejecutar código ajeno. safe_load solo construye tipos simples: diccionarios, listas, cadenas, números, booleanos y None.
Variables de entorno: todo es una cadena
Las claves de API, las direcciones de bases de datos, qué modelo usar: los valores que cambian de una máquina a otra o que tienen que mantenerse en secreto viven en variables de entorno, no en el código ni en una configuración versionada. Python las lee con os.environ u os.getenv. Para el ejemplo, dos variables se fijan dentro del propio código; en el trabajo real vienen de la shell, del servidor o del contenedor:
import os
os.environ["APP_DEBUG"] = "false"
os.environ["APP_PORT"] = "8000"
debug = os.getenv("APP_DEBUG")
print(repr(debug), bool(debug))
if debug:
print("debug mode is ON")
port = os.getenv("APP_PORT")
print(port + "1")
print(int(port) + 1)
print(os.getenv("LLM_API_KEY"))
print(os.getenv("LLM_TIMEOUT", "30"))'false' True
debug mode is ON
80001
8001
None
30Las variables de entorno son siempre cadenas. "false" es una cadena no vacía de cinco caracteres, así que if debug: la trata como verdadera; exactamente así el servidor del inicio del capítulo se ejecutó en modo depuración con "false" configurado. De la misma forma, "8000" + "1" es concatenación, no suma. Ningún error: solo un programa que hace algo mal.
Por eso, convierte cada variable de entorno al tipo correcto en cuanto la leas. Para números, basta con int() o float(): ellas mismas lanzan ValueError ante una entrada incorrecta. Para booleanos, Python no tiene conversión integrada (bool("false") es True), así que escribes la tuya:
def parse_bool(value: str) -> bool:
normalized = value.strip().lower()
if normalized in {"1", "true", "yes", "on"}:
return True
if normalized in {"0", "false", "no", "off"}:
return False
raise ValueError(f"not a boolean: {value!r}")
for raw in ["false", "True", " yes ", "0"]:
print(repr(raw), "->", parse_bool(raw))
parse_bool("flase")'false' -> False
'True' -> True
' yes ' -> True
'0' -> False
Traceback (most recent call last):
File "main.py", line 13, in <module>
parse_bool("flase")
File "main.py", line 7, in parse_bool
raise ValueError(f"not a boolean: {value!r}")
ValueError: not a boolean: 'flase'La última línea es lo que hace valiosa esta función. Un "flase" mal escrito no se convierte en silencio en True ni en False: el programa se detiene al inicio y dice qué valor no pudo entender.
Ten también presente la diferencia entre os.environ["NAME"] y os.getenv("NAME"). El primero lanza KeyError: 'LLM_API_KEY' cuando falta la variable; el segundo devuelve None en silencio, algo que puede aparecer minutos después como un 401 Unauthorized de una llamada a la API, momento en el que la causa es mucho más difícil de encontrar. Comprueba al inicio cualquier valor sin el que el programa no pueda funcionar.
El archivo .env: un entorno para tu propia máquina
Configurar diez variables cada vez que abres una terminal no es realista. La respuesta habitual es un archivo .env en la raíz del proyecto y el paquete python-dotenv, que lee ese archivo y pone los valores en os.environ:
uv add python-dotenvDe nuevo, el nombre del paquete y el del import son distintos: el paquete es python-dotenv, el import es dotenv. Un archivo .env:
# Local secrets. Never commit this file.
LLM_API_KEY=sk-test-placeholder-1234
LLM_MODEL=chat-small
APP_DEBUG=true(sk-test-placeholder-1234 es un valor inventado; nunca pongas una clave real en ningún documento, captura de pantalla ni commit.) El código para leerlo, construyendo de nuevo la ruta desde la ubicación del script:
import os
from pathlib import Path
from dotenv import load_dotenv
env_file = Path(__file__).resolve().parent / ".env"
found = load_dotenv(env_file)
print(found)
print(os.getenv("LLM_MODEL"))
print(os.getenv("APP_DEBUG"))
print(os.getenv("LLM_API_KEY")[:7] + "...")uv run main.py:
True
chat-small
true
sk-test...Ahora el mismo programa, pero con LLM_MODEL ya definida en la shell (macOS/Linux o Git Bash):
LLM_MODEL=chat-large uv run main.pyTrue
chat-large
true
sk-test...load_dotenv no reemplaza las variables que ya están definidas. Eso es deliberado e importante: .env es solo una comodidad para tu propia máquina, y el entorno real configurado en un servidor o en un contenedor siempre gana. (load_dotenv(path, override=True) invierte este orden, pero casi nunca hay razón para querer eso.)
Fíjate también en que load_dotenv no lanza error cuando falta el archivo: simplemente devuelve False. Así que nunca des por hecho que «se leyó el .env»; comprueba por separado los valores que necesitas, como hace el siguiente ejemplo.
Dos reglas que siguen todos los repositorios profesionales:
.envnunca se versiona. Va en.gitignore. Una vez que una clave ha entrado en el historial de Git, se queda allí aunque el archivo se borre; la única respuesta segura es revocar la clave y emitir una nueva..env.examplesí se versiona. Tiene los mismos nombres de variables con valores vacíos o inofensivos. Un nuevo compañero lo copia a.envy sabe exactamente qué valores hacen falta.
Un ejemplo completo
Ahora todo junto: un cargador de configuración para el bot de soporte. El proyecto se creó con uv init --app --python 3.12 support-bot, y después uv add pyyaml python-dotenv. La estructura:
support-bot/
├── .env
├── .env.example
├── config/
│ └── app.yaml
├── prompts/
│ ├── summarise.txt
│ └── system.txt
├── pyproject.toml
├── settings.py
└── uv.lockconfig/app.yaml: versionado, sin nada secreto:
app:
name: support-bot
debug: false
llm:
model: chat-small
temperature: 0.2
max_tokens: 512
retrieval:
top_k: 4.env: no versionado:
# Local values for this machine. Never commit this file.
LLM_API_KEY=sk-test-placeholder-1234
APP_DEBUG=true.env.example: versionado:
# Copy to .env and fill in. This file is committed; .env is not.
LLM_API_KEY=
APP_DEBUG=falseprompts/system.txt contiene una sola línea, You are a support assistant. Answer only from the provided context., y prompts/summarise.txt contiene Summarise the ticket below in two sentences.
Y settings.py:
import os
import sys
from pathlib import Path
import yaml
from dotenv import load_dotenv
PROJECT_ROOT = Path(__file__).resolve().parent
CONFIG_FILE = PROJECT_ROOT / "config" / "app.yaml"
PROMPTS_DIR = PROJECT_ROOT / "prompts"
class ConfigError(Exception):
"""Configuration is missing or invalid."""
def parse_bool(value: str) -> bool:
normalized = value.strip().lower()
if normalized in {"1", "true", "yes", "on"}:
return True
if normalized in {"0", "false", "no", "off"}:
return False
raise ConfigError(f"not a boolean: {value!r}")
def load_yaml(path: Path) -> dict:
if not path.is_file():
raise ConfigError(f"config file not found: {path.relative_to(PROJECT_ROOT).as_posix()}")
data = yaml.safe_load(path.read_text(encoding="utf-8"))
if not isinstance(data, dict):
raise ConfigError(f"{path.name} must contain a mapping at the top level")
return data
def load_prompts(folder: Path) -> dict[str, str]:
return {path.stem: path.read_text(encoding="utf-8").strip() for path in sorted(folder.glob("*.txt"))}
def require_env(name: str) -> str:
value = os.getenv(name, "").strip()
if not value:
raise ConfigError(f"missing required environment variable: {name}")
return value
def load_settings() -> dict:
load_dotenv(PROJECT_ROOT / ".env")
config = load_yaml(CONFIG_FILE)
temperature = float(os.getenv("LLM_TEMPERATURE", config["llm"]["temperature"]))
if not 0 <= temperature <= 2:
raise ConfigError(f"LLM_TEMPERATURE must be between 0 and 2, got {temperature}")
debug = config["app"]["debug"]
if "APP_DEBUG" in os.environ:
debug = parse_bool(os.environ["APP_DEBUG"])
return {
"app_name": config["app"]["name"],
"debug": debug,
"model": os.getenv("LLM_MODEL", config["llm"]["model"]),
"temperature": temperature,
"top_k": int(config["retrieval"]["top_k"]),
"api_key": require_env("LLM_API_KEY"),
"prompts": load_prompts(PROMPTS_DIR),
}
def mask(secret: str) -> str:
return secret[:3] + "*" * 8
if __name__ == "__main__":
try:
settings = load_settings()
except ConfigError as exc:
print(f"config error: {exc}")
sys.exit(1)
for key in ["app_name", "debug", "model", "temperature", "top_k"]:
print(f"{key:<12} {settings[key]}")
print(f"{'api_key':<12} {mask(settings['api_key'])}")
print(f"{'prompts':<12} {', '.join(settings['prompts'])}")Cuatro ejecuciones: normal, con dos valores cambiados desde la shell, con .env apartado y con una temperatura imposible:
uv run settings.py
LLM_TEMPERATURE=0 LLM_MODEL=chat-large uv run settings.py
uv run settings.py
LLM_TEMPERATURE=5 uv run settings.pyapp_name support-bot
debug True
model chat-small
temperature 0.2
top_k 4
api_key sk-********
prompts summarise, system
app_name support-bot
debug True
model chat-large
temperature 0.0
top_k 4
api_key sk-********
prompts summarise, system
config error: missing required environment variable: LLM_API_KEY
config error: LLM_TEMPERATURE must be between 0 and 2, got 5.0(Antes de la tercera ejecución se renombró el archivo .env para apartarlo, y se devolvió antes de la cuarta.) Las dos últimas ejecuciones terminan con código de salida 1 y las dos primeras con 0, así que un script de despliegue o el CI puede detectar fácilmente que la configuración es incorrecta.
Las decisiones detrás de este cargador:
Cada ruta sale de PROJECT_ROOT. Path(__file__).resolve().parent se calcula una sola vez, y todas las demás rutas se construyen sobre ella. Desde la carpeta que sea donde se inicie el programa, lee los mismos archivos.
Un orden de precedencia explícito. Primero los valores por defecto del YAML, después .env por encima, y el entorno real de la shell o del servidor por encima de todo. En la segunda ejecución, el modelo y la temperatura cambiaron sin editar un solo archivo, que es como se cambia la configuración en un solo lugar en producción.
temperature 0.0 sobrevive. os.getenv("LLM_TEMPERATURE", ...) solo recurre al valor del YAML cuando la variable de verdad no existe; no hay ningún or en ningún sitio. Del mismo modo, debug comprueba "APP_DEBUG" in os.environ, no si el valor es verdadero.
Conversión de tipos y comprobación de rangos en un solo lugar. float(), int(), parse_bool() y la comprobación 0 <= temperature <= 2 viven todos en load_settings. El resto del programa puede asumir que settings["temperature"] es un número válido.
Un ConfigError propio. Cada problema de configuración aparece como un solo tipo de error, y el bloque __main__ lo convierte en un mensaje claro de una línea (qué valor corregir), en vez de un traceback.
Los secretos nunca se imprimen completos. mask muestra los tres primeros caracteres, para que puedas saber qué tipo de clave se cargó, pero la clave nunca llega a los registros. La longitud de la máscara también es fija, así que no se puede deducir nada de la longitud de la clave.
Archivos de prompt con glob. Coloca un nuevo .txt en prompts/ y aparece en settings["prompts"] sin tocar el código. sorted() está ahí porque el orden en que glob devuelve los archivos depende del sistema operativo.
Una limitación: load_settings devuelve un diccionario simple, así que un error de escritura como settings["tempreature"] solo se detecta como KeyError en tiempo de ejecución, y cada nuevo ajuste necesita su conversión y su comprobación escritas a mano. Más adelante en el curso llegará una herramienta que hace esto de forma declarativa; los principios de aquí (cargar en un solo lugar, los tipos correctos, detenerse al inicio) siguen siendo los mismos con ella.
Cuando falla
FileNotFoundError: [Errno 2] No such file or directory: 'config\\app.json' La ruta es relativa y el programa se inició desde otra carpeta. Construye la ruta desde Path(__file__).resolve().parent. Para ver desde qué carpeta empieza la búsqueda, imprime Path.cwd().
TypeError: unsupported operand type(s) for /: 'str' and 'str' "config" / "app.json": cadenas en ambos lados. Haz que la parte de la izquierda sea un Path: Path("config") / "app.json".
json.decoder.JSONDecodeError: Expecting property name enclosed in double quotes: line 1 column 36 (char 35) El JSON tiene una coma sobrante después de su último elemento ({"top_k": 4,}), o una clave entre comillas simples. Los diccionarios de Python aceptan esto; JSON no. El número de columna del mensaje indica exactamente dónde se atascó el parser.
TypeError: load() missing 1 required positional argument: 'Loader' Código antiguo que llama a yaml.load(text). Escribe yaml.safe_load(text) en su lugar.
KeyError: 'LLM_API_KEY' El código leyó os.environ["LLM_API_KEY"] y la variable no está definida. Comprueba que .env esté en la raíz del proyecto, que load_dotenv recibió la ruta correcta y que el nombre se escribe igual en todas partes. Para dar un mensaje claro ante valores que faltan, usa una función como require_env de arriba.
ModuleNotFoundError: No module named 'dotenv' python-dotenv nunca se añadió al proyecto, o el programa se ejecutó sin uv run, así que no se usó el entorno del proyecto. Ejecuta uv add python-dotenv e inicia el programa con uv run.
Paso 4 de 6 — Predice
Comprueba lo que has entendido
El servidor tiene APP_DEBUG=false definido (aquí se muestra definiéndolo dentro del código). ¿Qué se imprime?
import os
os.environ["APP_DEBUG"] = "false"
debug = os.getenv("APP_DEBUG")
if debug:
print("debug on")
else:
print("debug off")- A`debug on`
- B`debug off`
- C`False`
- D`KeyError: 'APP_DEBUG'`
Para una ejecución de evaluación, la configuración fija temperature en 0 para que el modelo responda siempre igual. Pero las respuestas cambian en cada ejecución, y no hay ningún error. ¿Dónde está el fallo?
config = {"llm": {"model": "chat-small", "temperature": 0}}
temperature = config["llm"].get("temperature") or 0.7
print(f"calling {config['llm']['model']} at temperature {temperature}")- A`0` es falsy, así que `or` lo descarta y toma `0.7`; debería ser `config["llm"].get("temperature", 0.7)`
- B`.get("temperature")` no funciona con diccionarios anidados; debe ser `config["temperature"]`
- CUsar comillas simples dentro de la f-string cambia el valor de `temperature`
- DEl `0` que viene de JSON es en realidad la cadena `"0"`, así que la comparación falla
Este código vive en scripts/show_config.py, y la configuración está en config/app.json en la raíz del proyecto. Funciona desde la raíz y da FileNotFoundError desde scripts/. ¿Cuál es la mejor solución?
from pathlib import Path
config_path = Path("config/app.json")
print(config_path.read_text(encoding="utf-8"))- AEscribir la ruta como `"../config/app.json"`
- BGuardar también una copia de `config/app.json` dentro de la carpeta `scripts/`
- C`Path(__file__).resolve().parent.parent / "config" / "app.json"`: construir la ruta desde la ubicación propia del script
- DDecir a todos que el script solo debe ejecutarse desde la raíz del proyecto
Jugar es parte de Pro
+30 XP te esperan
Juega este capítulo con Pro
Leer es gratis y el capítulo está abierto entero. Los primeros 5 capítulos de cada curso gratuito se pueden jugar gratis. Desde el capítulo 6, responder las preguntas, ganar XP y hacer crecer tu racha es parte de Pro.
Desbloquea el juego con ProTu turno
Escribe un cargador de configuración que superponga una configuración específica de entorno sobre una configuración base. Crea un proyecto con uv init --app --python 3.12 config-lab, ejecuta uv add python-dotenv y después crea estos archivos:
config/base.json:{"llm": {"model": "chat-small", "temperature": 0.2}, "retrieval": {"top_k": 4}}config/staging.json:{"llm": {"temperature": 0}, "retrieval": {"top_k": 8}}.env: una sola línea,APP_ENV=staging
Escribe main.py de forma que:
- Toda ruta se construya desde
Path(__file__).resolve().parent, para que el programa dé el mismo resultado desde cualquier carpeta APP_ENVvenga del entorno o de.env, condevpor defecto; para un entorno sin archivo (comoconfig/prod.json), el programa imprimeconfig error: no config/prod.jsony se detiene con código de salida1- Las dos configuraciones se combinen en profundidad: los diccionarios anidados se combinan clave a clave en lugar de reemplazarse enteros, de modo que el
modeldebase.jsonsobrevive aunquestaging.jsonno tenga ninguno - Una
temperaturede0siga siendo0, sinor
Ejecutar uv run main.py desde la raíz del proyecto debe dar exactamente esto:
env staging
model chat-small
temperature 0
top_k 8Después, prueba dos cosas. Primero, ejecuta APP_ENV=prod uv run main.py y comprueba que el programa se detiene con un mensaje claro. Segundo, prueba un simple {**base, **overlay} en lugar de la combinación profunda e imprime config["llm"]: ¿adónde fue model, y por qué la línea de combinación en sí no lanzó ningún error?
Paso 6 de 6
Reto — el quiz del capítulo
Diez preguntas de fácil a difícil. Las últimas son difíciles a propósito.
Juega con Pro