pathlib, JSON/YAML-config en omgevingsvariabelen
Paden die vanuit elke map werken met pathlib, geneste JSON- en YAML-config, .env en omgevingsvariabelen — en een config-loader die bij de start stopt als een waarde fout is.
Hoe wil je leren?
Het probleem dat we oplossen
Een support-bot-project heeft een klein script, scripts/show_config.py, dat config/app.json leest en de modelnaam en de temperatuur toont. De auteur staat altijd in de hoofdmap van het project en draait python scripts/show_config.py, en dat werkt. Op een dag stapte een collega de map scripts/ in, draaide hetzelfde script en kreeg een FileNotFoundError — terwijl het bestand precies op de plek staat waar het altijd heeft gestaan.
Die week gebeurden er nog twee dingen. De server had APP_DEBUG=false ingesteld, maar in de logs stond dat de debugmodus aan stond. En iemand schreef temperature: 0 in de config, zodat het model elke keer hetzelfde antwoord zou geven — maar het programma draaide met 0.7. Geen van deze drie gaf een foutmelding.
Geen van de drie is een probleem met de logica van de code. Het probleem zit in de manier waarop het programma de buitenwereld leest — het bestandssysteem, configuratiebestanden en omgevingsvariabelen. In dit hoofdstuk maken we dat lezen betrouwbaar: paden met pathlib, geneste config met JSON en YAML, en geheimen met .env — elk met validatie, zodat een foute config al bij de start stopt in plaats van stilletjes door te draaien.
Aan het einde van dit hoofdstuk kun je
- Paden bouwen die op elk besturingssysteem werken met
pathlib.Path, met/in plaats van strings uitos.pathaan elkaar te plakken - Bestanden vinden vanaf de eigen locatie van het script, los van waar de terminal op dat moment staat
- Geneste JSON- en YAML-config lezen, en de manieren herkennen waarop
yaml.safe_loadstilletjes types verandert - Waarden uit omgevingsvariabelen en een
.env-bestand lezen, omzetten naar het juiste type en valideren - Een config-loader schrijven die stopt met een duidelijke melding wanneer een waarde fout of ontbrekend is
Wat je eerst nodig hebt: pyproject.toml, lockfiles en de src/-layout — vooral uv add en uv run.
pathlib: een pad is een object, geen string
In oudere Python-code was een pad een string, die je aan elkaar plakte met + "/" + of os.path.join. De Path-klasse uit de module pathlib behandelt een pad als een object waarvan je de onderdelen kunt opvragen:
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')Hier is / geen deling — gebruikt met een Path voegt het padonderdelen samen en kiest het het juiste scheidingsteken voor het besturingssysteem. De uitvoer wordt geprint met .as_posix(), zodat die er op elke machine hetzelfde uitziet; een gewone print(config_file) toont config\models\chat.yaml op Windows en config/models/chat.yaml op macOS en Linux.
Strings aan elkaar plakken maakt het verschil zichtbaar:
from pathlib import Path
base = "config"
print(base + "/" + "app.json")
print(Path(base) / "app.json")
print(repr(Path(base) / "app.json"))Op Windows print dit:
config/app.json
config\app.json
WindowsPath('config/app.json')De eerste regel is een met de hand gebouwde string. Windows accepteert / meestal wel, maar zodra je zulke paden aan andere tools doorgeeft of ze als string vergelijkt, zorgt het door elkaar gebruiken van twee soorten scheidingstekens voor verwarring. Path bouwt elk pad volgens de regels van het eigen systeem. Op macOS of Linux toont de tweede regel config/app.json en de derde PosixPath('config/app.json').
/werkt alleen als minstens één kant eenPathis. In"config" / "app.json"zijn beide kanten strings, dus Python leest het als deling en geeft eenTypeError. Maak het meest linkse deel eenPath, dan mogen de rest strings blijven.
Nog een paar methoden voor dagelijks werk, die de voorbeelden hieronder gebruiken:
path.exists(),path.is_file(),path.is_dir()— of het pad er is, en wat voor soort ding het ispath.read_text(encoding="utf-8")enpath.write_text(text, encoding="utf-8")— een heel bestand lezen of schrijven zonderopenpath.mkdir(parents=True, exist_ok=True)— een map maken, inclusief de tussenliggende mappen, zonder fout als die al bestaatfolder.glob("*.txt")— elk bestand in een map dat aan een patroon voldoet;rglobzoekt ook in submappenpath.resolve()— maakt van een relatief pad een absoluut pad
Schrijf altijd encoding="utf-8". Op Windows is de standaardcodering van Python vaak niet UTF-8. Een promptbestand met Bengaalse tekst of een emoji leest dan prima op een Mac, maar geeft op Windows verminkte tekens of een UnicodeDecodeError.
Ten opzichte van wat?
Het script uit het begin van het hoofdstuk ziet er zo uit:
from pathlib import Path
config_path = Path("config/app.json")
print(config_path.read_text(encoding="utf-8"))Vanuit de hoofdmap van het project print python scripts/show_config.py de config. Maar ga naar scripts/ en draai 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'(Op jouw machine toont de traceback de volledige paden; hier zijn ze ingekort.) Een relatief pad wordt altijd opgezocht vanaf de huidige werkmap — waar de terminal op dat moment staat — en niet vanaf de plek waar het scriptbestand staat. In scripts/ staat geen config/app.json, dus het bestand werd niet gevonden.
In productie gebeurt dit nog vaker: je weet niet altijd vanuit welke map een cronjob, een servicemanager of een container je programma start. De oplossing is het pad te koppelen aan de eigen locatie van het script. In elke module bevat __file__ het pad van dat bestand:
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}Nu print het dezelfde regel, of je het nu vanuit de hoofdmap draait, vanuit scripts/, of vanaf elders op de schijf met een volledig pad. Stap voor stap: Path(__file__) is show_config.py; .resolve() maakt het absoluut; de eerste .parent is scripts/; de tweede .parent is de hoofdmap van het project.
Laat.resolve()niet weg. In sommige situaties is__file__een relatief pad, en dan hangt.parent.parentweer af van de huidige map — precies het probleem dat je wilde vermijden.
JSON: geneste config en de nulwaarde-valkuil
Config in AI-projecten is bijna altijd genest — app-instellingen, modelinstellingen en retrieval-instellingen in aparte groepen. json.loads zet een JSON-string om in geneste dictionaries (uit een bestand: 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
0JSON's false wordt Python's False, en getallen blijven getallen — JSON houdt de types vast. Maar kijk naar de laatste twee regels. De config zegt duidelijk "temperature": 0, maar de eerste aanpak gaf 0.7.
x or default betekent "x als x truthy is, anders default". En in Python zijn 0, 0.0, "", [] en {} allemaal falsy. or kan dus niet onderscheiden tussen "geen waarde" en "een waarde, en die is nul". Een temperatuur van 0 zorgt dat het model elke keer het meest waarschijnlijke antwoord geeft, wat je wilt voor tests en evaluaties; or maakt er stilletjes 0.7 van, en wat je ziet zijn antwoorden die per run verschillen.
Gebruik.get(key, default)voor standaardwaarden — die gebruikt de default alleen als de sleutel echt ontbreekt.oris alleen goed als je ook nul- en lege waarden bewust wilt weggooien.
YAML: makkelijk voor mensen, maar het raadt de types
Veel teams schrijven config in YAML, omdat het commentaar toestaat en minder haakjes en aanhalingstekens nodig heeft. Python heeft geen ingebouwde YAML-lezer; voeg er een toe aan het project:
uv add pyyamlHet package heet pyyaml, maar je importeert het als import yaml. Nu een config lezen:
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 strAnders dan bij JSON hoef je de types in YAML niet op te schrijven — de parser raadt ze. Meestal is de gok goed, maar op twee plekken zit hij stilletjes fout. yes, no, on en off worden booleans — het bestand zei cache: off en je kreeg False. En 3.10 wordt gelezen als een kommagetal met de waarde 3.1; als versienummer is dat iets heel anders. De "3.10" met aanhalingstekens blijft een string.
De regel is eenvoudig: elke waarde die op een getal of een ja/nee lijkt maar eigenlijk tekst is — versienummers, ID's, postcodes — zet je in YAML tussen aanhalingstekens.
En gebruik altijd yaml.safe_load. De yaml.load(text) die je in oude tutorials ziet, geeft nu meteen de fout TypeError: load() missing 1 required positional argument: 'Loader'. Die wijziging is bewust: sommige loaders kunnen Python-objecten bouwen uit instructies in het YAML-bestand, dus het lezen van een onbetrouwbaar YAML-bestand betekent code van iemand anders uitvoeren. safe_load bouwt alleen eenvoudige types — dictionaries, lijsten, strings, getallen, booleans en None.
Omgevingsvariabelen: alles is een string
API-sleutels, databaseadressen, welk model je gebruikt — waarden die per machine verschillen of geheim moeten blijven, staan in omgevingsvariabelen, niet in code of in vastgelegde config. Python leest ze met os.environ of os.getenv. Voor het voorbeeld worden twee variabelen in de code zelf ingesteld; in echt werk komen ze uit de shell, de server of de container:
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
30Omgevingsvariabelen zijn altijd strings. "false" is een niet-lege string van vijf tekens, dus if debug: behandelt die als waar — precies zo draaide de server aan het begin van het hoofdstuk in debugmodus, terwijl "false" was ingesteld. Op dezelfde manier is "8000" + "1" aan elkaar plakken, niet optellen. Geen fout, alleen een programma dat het verkeerde doet.
Zet daarom elke omgevingsvariabele meteen na het lezen om naar het juiste type. Voor getallen is int() of float() genoeg — die geven zelf een ValueError bij een slechte invoer. Voor booleans heeft Python geen ingebouwde omzetting (bool("false") is True), dus schrijf je er zelf een:
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'De laatste regel is het hele nut van deze functie. Een verkeerd gespelde "flase" wordt niet stilletjes True of False — het programma stopt meteen bij de start en zegt welke waarde het niet begrijpt.
Houd ook het verschil tussen os.environ["NAME"] en os.getenv("NAME") in gedachten. De eerste geeft KeyError: 'LLM_API_KEY' als de variabele ontbreekt; de tweede geeft stilletjes None, wat een paar minuten later terugkomt als een 401 Unauthorized van een API-aanroep — en dan is de oorzaak veel lastiger te vinden. Controleer aan het allereerste begin elke waarde waar het programma niet zonder kan.
Het .env-bestand: een omgeving voor je eigen machine
Elke keer bij het openen van een terminal tien variabelen instellen is niet realistisch. Het gebruikelijke antwoord is een .env-bestand in de hoofdmap van het project en het package python-dotenv, dat dat bestand leest en de waarden in os.environ zet:
uv add python-dotenvOok hier verschillen de packagenaam en de importnaam: het package is python-dotenv, de import is dotenv. Een .env-bestand:
# Local secrets. Never commit this file.
LLM_API_KEY=sk-test-placeholder-1234
LLM_MODEL=chat-small
APP_DEBUG=true(sk-test-placeholder-1234 is een verzonnen waarde; zet nooit een echte sleutel in een document, screenshot of commit.) De code om het te lezen, en ook hier wordt het pad opgebouwd vanaf de locatie van het 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...Nu hetzelfde programma, maar met LLM_MODEL al ingesteld in de shell (macOS/Linux of Git Bash):
LLM_MODEL=chat-large uv run main.pyTrue
chat-large
true
sk-test...load_dotenv vervangt variabelen die al zijn ingesteld niet. Dat is bewust en belangrijk: .env is alleen een gemak voor je eigen machine, en de echte omgeving op een server of in een container wint altijd. (load_dotenv(path, override=True) keert deze volgorde om, maar er is bijna nooit een reden om dat te willen.)
Let ook op dat load_dotenv geen fout geeft als het bestand ontbreekt — het geeft gewoon False terug. Neem dus nooit aan dat .env is gelezen; controleer de waarden die je nodig hebt apart, zoals het volgende voorbeeld doet.
Twee regels die elke professionele repository volgt:
.envwordt nooit gecommit. Het staat in.gitignore. Zodra een sleutel in de Git-geschiedenis staat, blijft hij daar staan, ook nadat het bestand is verwijderd; de enige veilige reactie is de sleutel intrekken en een nieuwe aanmaken..env.examplewordt wel gecommit. Het bevat dezelfde variabelenamen met lege of onschadelijke waarden. Een nieuwe collega kopieert het naar.enven weet precies welke waarden nodig zijn.
Een compleet voorbeeld
Nu alles samen: een config-loader voor de support-bot. Het project is gemaakt met uv init --app --python 3.12 support-bot, daarna uv add pyyaml python-dotenv. De structuur:
support-bot/
├── .env
├── .env.example
├── config/
│ └── app.yaml
├── prompts/
│ ├── summarise.txt
│ └── system.txt
├── pyproject.toml
├── settings.py
└── uv.lockconfig/app.yaml — gecommit, er staat niets geheims in:
app:
name: support-bot
debug: false
llm:
model: chat-small
temperature: 0.2
max_tokens: 512
retrieval:
top_k: 4.env — niet gecommit:
# Local values for this machine. Never commit this file.
LLM_API_KEY=sk-test-placeholder-1234
APP_DEBUG=true.env.example — gecommit:
# Copy to .env and fill in. This file is committed; .env is not.
LLM_API_KEY=
APP_DEBUG=falseprompts/system.txt bevat één regel, You are a support assistant. Answer only from the provided context., en prompts/summarise.txt bevat Summarise the ticket below in two sentences.
En 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'])}")Vier runs — normaal, met twee waarden veranderd vanuit de shell, met .env opzij gezet, en met een onmogelijke temperatuur:
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(Voor de derde run is het .env-bestand opzij gezet, en het is teruggezet voor de vierde.) De laatste twee runs eindigen met exitcode 1 en de eerste twee met 0, zodat een deploy-script of CI makkelijk kan zien dat de config fout is.
De keuzes achter deze loader:
Elk pad komt uit PROJECT_ROOT. Path(__file__).resolve().parent wordt één keer berekend en elk ander pad is daarop gebaseerd. Vanuit welke map het programma ook start, het leest altijd dezelfde bestanden.
Eén duidelijke volgorde van voorrang. Eerst de YAML-standaardwaarden, .env daarboven, en de echte omgeving van de shell of server boven alles. In de tweede run veranderden het model en de temperatuur zonder dat er één bestand is aangepast — zo wordt config in productie op één plek veranderd.
temperature 0.0 blijft behouden. os.getenv("LLM_TEMPERATURE", ...) valt alleen terug op de YAML-waarde als de variabele echt ontbreekt — er staat nergens een or. Ook debug controleert "APP_DEBUG" in os.environ, en niet of de waarde truthy is.
Typeomzetting en bereikcontroles op één plek. float(), int(), parse_bool() en de controle 0 <= temperature <= 2 staan allemaal in load_settings. De rest van het programma mag ervan uitgaan dat settings["temperature"] een geldig getal is.
Een eigen ConfigError. Elk configprobleem komt als dezelfde soort fout naar boven, en het __main__-blok maakt er één duidelijke regel van — welke waarde je moet repareren, in plaats van een traceback.
Geheimen worden nooit volledig geprint. mask toont de eerste drie tekens, zodat je ziet welk soort sleutel is geladen, maar de sleutel komt nooit in de logs. Ook de lengte van het masker is vast, zodat er niets over de lengte van de sleutel af te leiden valt.
Promptbestanden via glob. Zet een nieuw .txt-bestand in prompts/ en het verschijnt in settings["prompts"] zonder dat je de code aanraakt. sorted() staat erbij omdat de volgorde waarin glob bestanden teruggeeft afhangt van het besturingssysteem.
Eén beperking: load_settings geeft een gewone dictionary terug, dus een typfout als settings["tempreature"] wordt pas tijdens het draaien opgemerkt als KeyError, en elke nieuwe instelling vraagt om de omzetting en controle met de hand. Een tool die dit declaratief doet, komt later in de cursus; de principes hier — één plek laden, de juiste types, stoppen bij de start — blijven hetzelfde met zo'n tool.
Wanneer het breekt
FileNotFoundError: [Errno 2] No such file or directory: 'config\\app.json' Het pad is relatief en het programma is vanuit een andere map gestart. Bouw het pad op met Path(__file__).resolve().parent. Om te zien vanuit welke map het zoeken begint, print je Path.cwd().
TypeError: unsupported operand type(s) for /: 'str' and 'str' "config" / "app.json" — strings aan beide kanten. Maak het meest linkse deel een Path: Path("config") / "app.json".
json.decoder.JSONDecodeError: Expecting property name enclosed in double quotes: line 1 column 36 (char 35) De JSON heeft een komma na het laatste item ({"top_k": 4,}), of een sleutel tussen enkele aanhalingstekens. Python-dictionaries accepteren dat; JSON niet. Het kolomnummer in de melding laat precies zien waar de parser is blijven hangen.
TypeError: load() missing 1 required positional argument: 'Loader' Oude code die yaml.load(text) aanroept. Schrijf in plaats daarvan yaml.safe_load(text).
KeyError: 'LLM_API_KEY' De code las os.environ["LLM_API_KEY"] en de variabele is niet ingesteld. Controleer of .env in de hoofdmap van het project staat, of load_dotenv het juiste pad kreeg, en of de naam overal hetzelfde is gespeld. Gebruik voor een duidelijke melding bij ontbrekende waarden een functie zoals require_env hierboven.
ModuleNotFoundError: No module named 'dotenv' python-dotenv is nooit aan het project toegevoegd, of het programma is zonder uv run gestart, waardoor de omgeving van het project niet is gebruikt. Voer uv add python-dotenv uit en start het programma met uv run.
Stap 4 van 6 — Voorspellen
Test je begrip
De server heeft APP_DEBUG=false ingesteld (hier getoond door het in de code in te stellen). Wat wordt geprint?
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'`
Voor een evaluatieronde zet de config temperature op 0, zodat het model elke keer hetzelfde antwoord geeft. Maar de antwoorden verschillen bij elke run, en er is geen fout. Waar zit de 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` is falsy, dus `or` gooit het weg en neemt `0.7`; het moet `config["llm"].get("temperature", 0.7)` zijn
- B`.get("temperature")` werkt niet op geneste dictionaries; het moet `config["temperature"]` zijn
- CEnkele aanhalingstekens binnen de f-string veranderen de waarde van `temperature`
- DDe `0` uit JSON is eigenlijk de string `"0"`, dus de vergelijking mislukt
Deze code staat in scripts/show_config.py, en de config staat in config/app.json in de hoofdmap van het project. Het werkt vanuit de hoofdmap en geeft FileNotFoundError vanuit scripts/. Wat is de beste oplossing?
from pathlib import Path
config_path = Path("config/app.json")
print(config_path.read_text(encoding="utf-8"))- ASchrijf het pad als `"../config/app.json"`
- BBewaar ook een kopie van `config/app.json` in de map `scripts/`
- C`Path(__file__).resolve().parent.parent / "config" / "app.json"` — bouw het pad vanaf de eigen locatie van het script
- DVertel iedereen dat het script alleen vanuit de hoofdmap van het project mag worden gedraaid
Spelen hoort bij Pro
+30 XP wacht op je
Speel dit hoofdstuk met Pro
Lezen is gratis en het hele hoofdstuk is open. De eerste 5 hoofdstukken van elke gratis cursus speel je gratis. Vanaf hoofdstuk 6 horen vragen beantwoorden, XP verdienen en je reeks laten groeien bij Pro.
Ontgrendel spelen met ProJouw beurt
Schrijf een config-loader die een omgevingsspecifieke config bovenop een basisconfig legt. Maak een project met uv init --app --python 3.12 config-lab, voer uv add python-dotenv uit en maak daarna deze bestanden:
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: één regel,APP_ENV=staging
Schrijf main.py zo dat:
- Elk pad wordt opgebouwd vanaf
Path(__file__).resolve().parent, zodat het programma vanuit elke map hetzelfde resultaat geeft APP_ENVkomt uit de omgeving of uit.env, metdevals standaard; voor een omgeving zonder bestand (zoalsconfig/prod.json) print het programmaconfig error: no config/prod.jsonen stopt het met exitcode1- De twee configs worden diep samengevoegd: geneste dictionaries worden sleutel voor sleutel samengevoegd in plaats van als geheel vervangen, zodat
modeluitbase.jsonblijft bestaan, ook al heeftstaging.jsondie niet - Een
temperaturevan0blijft0— zonderor
Vanuit de hoofdmap van het project zou uv run main.py precies dit moeten geven:
env staging
model chat-small
temperature 0
top_k 8Test daarna twee dingen. Draai eerst APP_ENV=prod uv run main.py en controleer of het programma stopt met een duidelijke melding. Test daarnaast een gewone {**base, **overlay} in plaats van de deep merge, en print config["llm"] — waar is model gebleven, en waarom gaf de merge-regel zelf geen fout?
Stap 6 van 6
Uitdaging — de hoofdstukquiz
Tien vragen van makkelijk naar moeilijk. De laatste zijn met opzet lastig.
Speel met Pro