pathlib, configuration JSON/YAML et variables d'environnement
Des chemins qui fonctionnent depuis n'importe quel dossier avec pathlib, une configuration JSON et YAML imbriquée, .env et variables d'environnement — et un chargeur de configuration qui s'arrête dès le départ quand une valeur est fausse.
Comment voulez-vous apprendre ?
Le problème que nous résolvons
Un projet de bot de support contient un petit script, scripts/show_config.py, qui lit config/app.json et affiche le nom du modèle et la température. Son auteur se place toujours à la racine du projet et lance python scripts/show_config.py, et ça fonctionne. Un jour, un collègue s'est placé dans le dossier scripts/, a lancé le même script, et a obtenu une FileNotFoundError — alors que le fichier est exactement là où il a toujours été.
Deux autres choses se sont produites cette semaine-là. Le serveur avait APP_DEBUG=false défini, pourtant les journaux montraient que le mode débogage était activé. Et quelqu'un avait écrit temperature: 0 dans la configuration pour que le modèle donne toujours la même réponse — mais le programme tournait avec 0.7. Aucune de ces situations n'a levé d'erreur.
Aucun de ces trois problèmes ne vient de la logique du code. Le problème tient à la façon dont le programme lit le monde extérieur — le système de fichiers, les fichiers de configuration, les variables d'environnement. Dans ce chapitre, nous rendons cette lecture fiable : des chemins avec pathlib, une configuration imbriquée en JSON et en YAML, et les secrets avec .env — chacun avec une validation, pour qu'une mauvaise configuration s'arrête dès le départ au lieu de tourner silencieusement.
À la fin de ce chapitre vous pourrez
- Construire des chemins qui fonctionnent sur n'importe quel système d'exploitation avec
pathlib.Path, en utilisant/au lieu d'assembler des chaînes avecos.path - Trouver des fichiers à partir de l'emplacement du script lui-même, indépendamment de l'endroit où se trouve le terminal
- Lire une configuration JSON et YAML imbriquée, et reconnaître les façons dont
yaml.safe_loadmodifie discrètement les types - Lire des valeurs depuis des variables d'environnement et un fichier
.env, les convertir dans le bon type et les valider - Écrire un chargeur de configuration qui s'arrête avec un message clair quand une valeur est fausse ou manquante
Ce qu'il vous faut d'abord : pyproject.toml, lockfiles et layout src/ — surtout uv add et uv run.
pathlib : un chemin est un objet, pas une chaîne
Dans l'ancien code Python, un chemin était une chaîne de caractères, assemblée avec + "/" + ou os.path.join. La classe Path du module pathlib traite un chemin comme un objet dont on peut demander les différentes parties :
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')Ici / n'est pas une division — utilisé avec un Path, il assemble les parties du chemin et choisit le bon séparateur pour le système d'exploitation. La sortie est affichée avec .as_posix() pour qu'elle soit identique sur toutes les machines ; un simple print(config_file) affiche config\models\chat.yaml sous Windows et config/models/chat.yaml sous macOS et Linux.
Assembler des chaînes montre la différence :
from pathlib import Path
base = "config"
print(base + "/" + "app.json")
print(Path(base) / "app.json")
print(repr(Path(base) / "app.json"))Sous Windows, cela affiche :
config/app.json
config\app.json
WindowsPath('config/app.json')La première ligne est un chemin construit à la main. Windows accepte généralement /, mais dès qu'on transmet ce type de chemin à d'autres outils ou qu'on les compare comme des chaînes, deux sortes de séparateurs mélangés provoquent de la confusion. Path construit chaque chemin selon les règles de son propre système. Sous macOS ou Linux, la deuxième ligne affiche config/app.json et la troisième PosixPath('config/app.json').
/ne fonctionne que si au moins un des deux côtés est unPath. Dans"config" / "app.json", les deux côtés sont des chaînes ; Python lit donc une division et lève uneTypeError. Faites du membre de gauche unPath, et le reste peut rester en chaînes.
Quelques autres méthodes utiles au quotidien, que les exemples ci-dessous utilisent :
path.exists(),path.is_file(),path.is_dir()— indiquent si le chemin existe, et de quel type d'élément il s'agitpath.read_text(encoding="utf-8")etpath.write_text(text, encoding="utf-8")— lisent ou écrivent un fichier entier sansopenpath.mkdir(parents=True, exist_ok=True)— crée un dossier, ainsi que les dossiers intermédiaires, sans erreur s'il existe déjàfolder.glob("*.txt")— tous les fichiers d'un dossier correspondant à un motif ;rglobcherche aussi dans les sous-dossierspath.resolve()— transforme un chemin relatif en chemin absolu
Écrivez encoding="utf-8" à chaque fois. Sous Windows, l'encodage par défaut de Python n'est souvent pas UTF-8 : un fichier de prompt contenant du bengali ou un emoji se lit correctement sur un Mac, mais donne des caractères illisibles, voire une UnicodeDecodeError, sous Windows.
Relatif à quoi ?
Le script du début du chapitre ressemble à ceci :
from pathlib import Path
config_path = Path("config/app.json")
print(config_path.read_text(encoding="utf-8"))Depuis la racine du projet, python scripts/show_config.py affiche la configuration. Mais placez-vous dans scripts/ et lancez 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'(Sur votre machine, le traceback affiche les chemins complets ; ils sont raccourcis ici.) Un chemin relatif est toujours recherché à partir du dossier de travail courant — là où se trouve le terminal — et non à partir de l'emplacement du fichier du script. Il n'y a pas de config/app.json dans scripts/, donc le fichier n'est pas trouvé.
En production, c'est encore plus vrai : on ne sait pas toujours depuis quel dossier une tâche cron, un gestionnaire de services ou un conteneur lancera le programme. La solution consiste à rattacher le chemin à l'emplacement du script lui-même. Dans chaque module, __file__ contient le chemin de ce fichier :
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}Maintenant, le programme affiche la même ligne, qu'on le lance depuis la racine du projet, depuis scripts/, ou depuis n'importe quel autre endroit du disque avec un chemin complet. Pas à pas : Path(__file__) est show_config.py ; .resolve() le rend absolu ; le premier .parent est scripts/ ; le second .parent est la racine du projet.
Ne supprimez pas.resolve(). Dans certaines situations,__file__est un chemin relatif ;.parent.parentdépend alors à nouveau du dossier courant — le problème même que vous cherchiez à éviter.
JSON : configuration imbriquée et le piège du zéro
La configuration des projets d'IA est presque toujours imbriquée : paramètres de l'application, du modèle et de la recherche, dans des groupes séparés. json.loads transforme une chaîne JSON en dictionnaires imbriqués (depuis un fichier, 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
0Le false de JSON devient False en Python, et les nombres restent des nombres — JSON conserve les types. Mais regardez les deux dernières lignes. La configuration indique clairement "temperature": 0, pourtant la première approche donne 0.7.
x or default signifie « x si x est vrai, sinon default ». Or, en Python, 0, 0.0, "", [] et {} sont tous faux. or ne peut donc pas distinguer « pas de valeur » de « une valeur, et elle vaut zéro ». Une température de 0 fait donner au modèle sa réponse la plus probable à chaque fois, ce qui est exactement ce qu'on veut pour les tests et l'évaluation ; or la transforme discrètement en 0.7, et tout ce que vous voyez, ce sont des réponses qui changent d'un lancement à l'autre.
Utilisez.get(key, default)pour les valeurs par défaut — la valeur par défaut n'est utilisée que si la clé est réellement absente.orne convient que lorsque vous voulez aussi écarter le zéro et les valeurs vides.
YAML : agréable pour les humains, mais il devine les types
Beaucoup d'équipes écrivent leur configuration en YAML, car il permet les commentaires et comporte moins de crochets et de guillemets. Python ne fournit pas de lecteur YAML ; ajoutez-en un au projet :
uv add pyyamlLe paquet s'appelle pyyaml, mais on l'importe avec import yaml. Lisons maintenant une configuration :
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 strContrairement à JSON, YAML ne vous oblige pas à écrire les types — le parseur les devine. La plupart du temps, la devinette est juste, mais à deux endroits elle est discrètement fausse. yes, no, on et off deviennent des booléens — le fichier disait cache: off et vous obtenez False. Et 3.10 est lu comme un nombre décimal dont la valeur est 3.1 ; pour un numéro de version, c'est tout autre chose. La valeur entre guillemets "3.10" reste une chaîne.
La règle est simple : toute valeur qui ressemble à un nombre ou à un oui/non mais qui est en réalité du texte — numéros de version, identifiants, codes postaux — se met entre guillemets en YAML.
Et utilisez toujours yaml.safe_load. Le yaml.load(text) que l'on voit dans les anciens tutoriels échoue désormais avec TypeError: load() missing 1 required positional argument: 'Loader'. Ce changement est voulu : certains chargeurs peuvent construire des objets Python arbitraires à partir d'instructions contenues dans le fichier YAML ; lire un fichier YAML non fiable revient donc à exécuter le code de quelqu'un d'autre. safe_load ne construit que des types simples — dictionnaires, listes, chaînes, nombres, booléens, None.
Variables d'environnement : tout est une chaîne
Les clés d'API, les adresses de bases de données, le modèle à utiliser — les valeurs qui changent d'une machine à l'autre ou qui doivent rester secrètes vivent dans des variables d'environnement, et non dans le code ni dans une configuration versionnée. Python les lit avec os.environ ou os.getenv. Pour l'exemple, deux variables sont définies dans le code lui-même ; en pratique, elles viennent du shell, du serveur ou du conteneur :
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
30Les variables d'environnement sont toujours des chaînes. "false" est une chaîne non vide de cinq caractères, donc if debug: la traite comme vraie — c'est exactement ce qui a fait tourner en mode débogage le serveur du début du chapitre, alors que « false » était défini. De la même façon, "8000" + "1" est une concaténation, et non une addition. Aucune erreur, juste un programme qui fait la mauvaise chose.
Convertissez donc chaque variable d'environnement dans le bon type dès que vous la lisez. Pour les nombres, int() ou float() suffisent — elles lèvent elles-mêmes une ValueError sur une valeur incorrecte. Pour les booléens, Python n'a pas de conversion intégrée (bool("false") vaut True) ; écrivez donc la vôtre :
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 dernière ligne est tout l'intérêt de cette fonction. Une faute de frappe comme "flase" ne devient pas discrètement True ou False — le programme s'arrête dès le départ et indique la valeur qu'il n'a pas su comprendre.
Gardez aussi en tête la différence entre os.environ["NAME"] et os.getenv("NAME"). La première lève KeyError: 'LLM_API_KEY' si la variable manque ; la seconde renvoie discrètement None, ce qui peut se manifester quelques minutes plus tard par un 401 Unauthorized d'un appel d'API — et la cause est alors bien plus difficile à trouver. Vérifiez dès le début toute valeur sans laquelle le programme ne peut pas tourner.
Le fichier .env : un environnement pour votre propre machine
Régler dix variables à chaque ouverture d'un terminal n'est pas réaliste. La réponse habituelle est un fichier .env à la racine du projet et le paquet python-dotenv, qui lit ce fichier et place les valeurs dans os.environ :
uv add python-dotenvLà encore, le nom du paquet et celui de l'import diffèrent : le paquet est python-dotenv, l'import est dotenv. Un fichier .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 est une valeur fictive ; ne mettez jamais une vraie clé dans un document, une capture d'écran ou un commit.) Le code pour le lire, là encore en construisant le chemin à partir de l'emplacement du 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...Voici maintenant le même programme, mais avec LLM_MODEL déjà défini dans le shell (macOS/Linux ou Git Bash) :
LLM_MODEL=chat-large uv run main.pyTrue
chat-large
true
sk-test...load_dotenv ne remplace pas les variables déjà définies. C'est voulu et important : .env n'est qu'un confort pour votre propre machine, et l'environnement réel défini sur un serveur ou dans un conteneur prime toujours. (load_dotenv(path, override=True) inverse cet ordre, mais il n'y a presque jamais de raison de le souhaiter.)
Remarquez aussi que load_dotenv ne lève pas d'erreur quand le fichier est absent — il renvoie simplement False. Ne supposez donc jamais que « .env a été lu » ; vérifiez séparément les valeurs dont vous avez besoin, comme le fait l'exemple suivant.
Deux règles que suit tout dépôt professionnel :
.envn'est jamais versionné. Il va dans.gitignore. Une fois une clé entrée dans l'historique Git, elle y reste même après la suppression du fichier ; la seule réponse sûre est de révoquer la clé et d'en générer une nouvelle..env.exampleest versionné. Il contient les mêmes noms de variables, avec des valeurs vides ou inoffensives. Un nouveau collègue le copie en.envet sait exactement quelles valeurs sont nécessaires.
Un exemple complet
Rassemblons maintenant tout cela : un chargeur de configuration pour le bot de support. Le projet a été créé avec uv init --app --python 3.12 support-bot, puis uv add pyyaml python-dotenv. L'arborescence :
support-bot/
├── .env
├── .env.example
├── config/
│ └── app.yaml
├── prompts/
│ ├── summarise.txt
│ └── system.txt
├── pyproject.toml
├── settings.py
└── uv.lockconfig/app.yaml — versionné, sans aucun secret :
app:
name: support-bot
debug: false
llm:
model: chat-small
temperature: 0.2
max_tokens: 512
retrieval:
top_k: 4.env — non versionné :
# Local values for this machine. Never commit this file.
LLM_API_KEY=sk-test-placeholder-1234
APP_DEBUG=true.env.example — versionné :
# Copy to .env and fill in. This file is committed; .env is not.
LLM_API_KEY=
APP_DEBUG=falseprompts/system.txt ne contient qu'une ligne, You are a support assistant. Answer only from the provided context., et prompts/summarise.txt contient Summarise the ticket below in two sentences.
Et 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'])}")Quatre exécutions — normale, avec deux valeurs modifiées depuis le shell, avec .env mis de côté, et avec une température impossible :
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(Avant la troisième exécution, le fichier .env a été renommé pour le mettre de côté, et il a été remis en place avant la quatrième.) Les deux dernières exécutions se terminent avec le code de sortie 1 et les deux premières avec 0, si bien qu'un script de déploiement ou la CI peut facilement savoir que la configuration est fausse.
Les choix qui sous-tendent ce chargeur :
Tous les chemins partent de PROJECT_ROOT. Path(__file__).resolve().parent est calculé une seule fois, et tous les autres chemins en découlent. Quel que soit le dossier depuis lequel le programme est lancé, il lit les mêmes fichiers.
Un ordre de priorité explicite. Les valeurs par défaut du YAML d'abord, .env au-dessus, et l'environnement réel du shell ou du serveur au-dessus de tout. Dans la deuxième exécution, le modèle et la température ont changé sans modifier un seul fichier — c'est ainsi qu'on modifie la configuration à un seul endroit en production.
temperature 0.0 est conservé. os.getenv("LLM_TEMPERATURE", ...) ne revient à la valeur du YAML que lorsque la variable est réellement absente — il n'y a aucun or nulle part. De même, debug teste "APP_DEBUG" in os.environ, et non si la valeur est vraie.
Conversion de types et contrôles de valeurs au même endroit. float(), int(), parse_bool() et la vérification 0 <= temperature <= 2 se trouvent tous dans load_settings. Le reste du programme peut supposer que settings["temperature"] est un nombre valide.
Une ConfigError dédiée. Chaque problème de configuration remonte sous un seul type d'erreur, et le bloc __main__ le transforme en un message clair d'une ligne — quelle valeur corriger — au lieu d'un traceback.
Les secrets ne sont jamais affichés en entier. mask n'affiche que les trois premiers caractères, ce qui permet de savoir quel type de clé a été chargé, mais la clé n'atteint jamais les journaux. La longueur du masque est aussi fixe, si bien que rien ne peut être déduit de la longueur de la clé.
Fichiers de prompts avec glob. Déposez un nouveau .txt dans prompts/ et il apparaît dans settings["prompts"] sans toucher au code. sorted() est là parce que l'ordre dans lequel glob renvoie les fichiers dépend du système d'exploitation.
Une limite : load_settings renvoie un dictionnaire simple, donc une faute de frappe comme settings["tempreature"] n'est détectée qu'à l'exécution, sous forme de KeyError, et chaque nouveau paramètre doit recevoir sa conversion et sa vérification à la main. Un outil qui fait cela de façon déclarative arrivera plus tard dans le cours ; les principes ici — charger à un seul endroit, utiliser les bons types, s'arrêter dès le départ — restent les mêmes avec lui.
Quand ça casse
FileNotFoundError: [Errno 2] No such file or directory: 'config\\app.json' Le chemin est relatif et le programme a été lancé depuis un autre dossier. Construisez le chemin à partir de Path(__file__).resolve().parent. Pour voir à partir de quel dossier la recherche commence, affichez Path.cwd().
TypeError: unsupported operand type(s) for /: 'str' and 'str' "config" / "app.json" — des chaînes des deux côtés. Faites du membre de gauche un Path : Path("config") / "app.json".
json.decoder.JSONDecodeError: Expecting property name enclosed in double quotes: line 1 column 36 (char 35) Le JSON contient une virgule finale après son dernier élément ({"top_k": 4,}), ou une clé entre guillemets simples. Les dictionnaires Python acceptent ces écritures ; pas JSON. Le numéro de colonne dans le message indique exactement où le parseur s'est arrêté.
TypeError: load() missing 1 required positional argument: 'Loader' Ancien code qui appelle yaml.load(text). Écrivez plutôt yaml.safe_load(text).
KeyError: 'LLM_API_KEY' Le code a lu os.environ["LLM_API_KEY"] et la variable n'est pas définie. Vérifiez que .env se trouve à la racine du projet, que load_dotenv a reçu le bon chemin, et que le nom est écrit de la même façon partout. Pour un message clair sur les valeurs manquantes, utilisez une fonction comme require_env ci-dessus.
ModuleNotFoundError: No module named 'dotenv' python-dotenv n'a jamais été ajouté au projet, ou le programme a été lancé sans uv run, si bien que l'environnement du projet n'a pas été utilisé. Lancez uv add python-dotenv, et démarrez le programme avec uv run.
Étape 4 sur 6 — Prédire
Vérifiez votre compréhension
Le serveur a APP_DEBUG=false défini (on le fixe ici directement dans le code). Qu'est-ce qui s'affiche ?
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'`
Pour une évaluation, la configuration règle temperature sur 0 pour que le modèle réponde toujours de la même façon. Pourtant, les réponses changent à chaque exécution, et aucune erreur n'apparaît. Où est le bug ?
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` est faux, donc `or` l'écarte et prend `0.7` ; il faudrait `config["llm"].get("temperature", 0.7)`
- B`.get("temperature")` ne fonctionne pas sur les dictionnaires imbriqués ; il faut `config["temperature"]`
- CUtiliser des apostrophes à l'intérieur de la f-string modifie la valeur de `temperature`
- DLe `0` issu du JSON est en réalité la chaîne `"0"`, donc la comparaison échoue
Ce code se trouve dans scripts/show_config.py, et la configuration est dans config/app.json à la racine du projet. Il fonctionne depuis la racine et donne FileNotFoundError depuis scripts/. Quelle est la meilleure correction ?
from pathlib import Path
config_path = Path("config/app.json")
print(config_path.read_text(encoding="utf-8"))- AÉcrire le chemin sous la forme `"../config/app.json"`
- BGarder aussi une copie de `config/app.json` dans le dossier `scripts/`
- C`Path(__file__).resolve().parent.parent / "config" / "app.json"` — construire le chemin à partir de l'emplacement du script lui-même
- DDire à tout le monde que le script ne doit être lancé que depuis la racine du projet
Jouer fait partie de Pro
+30 XP vous attendent
Jouez ce chapitre avec Pro
La lecture est gratuite et tout le chapitre est ouvert. Les 5 premiers chapitres de chaque cours gratuit se jouent gratuitement. À partir du chapitre 6, répondre aux questions, gagner des XP et faire grandir votre série fait partie de Pro.
Débloquer le jeu avec ProÀ vous de jouer
Écrivez un chargeur de configuration qui superpose une configuration propre à un environnement sur une configuration de base. Créez un projet avec uv init --app --python 3.12 config-lab, lancez uv add python-dotenv, puis créez ces fichiers :
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: une seule ligne,APP_ENV=staging
Écrivez main.py de façon à ce que :
- Chaque chemin soit construit à partir de
Path(__file__).resolve().parent, pour que le programme donne le même résultat depuis n'importe quel dossier APP_ENVvienne de l'environnement ou de.env, avecdevpar défaut ; pour un environnement sans fichier (commeconfig/prod.json), le programme afficheconfig error: no config/prod.jsonet s'arrête avec le code de sortie1- Les deux configurations soient fusionnées en profondeur : les dictionnaires imbriqués fusionnent clé par clé au lieu d'être remplacés en bloc, de sorte que le
modeldebase.jsonsurvive même sistaging.jsonn'en a pas - Une
temperaturede0reste0— pas deor
Lancer uv run main.py depuis la racine du projet doit donner exactement ceci :
env staging
model chat-small
temperature 0
top_k 8Testez ensuite deux choses. D'abord, lancez APP_ENV=prod uv run main.py et vérifiez que le programme s'arrête avec un message clair. Ensuite, essayez un simple {**base, **overlay} à la place de la fusion profonde et affichez config["llm"] — où est passé model, et pourquoi la ligne de fusion elle-même n'a levé aucune erreur ?
Étape 6 sur 6
Défi — le quiz du chapitre
Dix questions, de la plus facile à la plus difficile. Les dernières sont difficiles exprès.
Jouer avec Pro