pathlib, JSON-/YAML-Config und Umgebungsvariablen
Pfade, die aus jedem Ordner funktionieren, mit pathlib, verschachtelte JSON- und YAML-Config, .env und Umgebungsvariablen — und ein Config-Loader, der am Start stoppt, wenn ein Wert falsch ist.
Wie möchtest du lernen?
Das Problem, das wir lösen
Ein Support-Bot-Projekt hat ein kleines Skript, scripts/show_config.py, das config/app.json liest und Modellname und Temperatur anzeigt. Sein Autor steht immer im Projektstamm und führt python scripts/show_config.py aus, und es funktioniert. Eines Tages ist ein Teammitglied in den Ordner scripts/ gewechselt, hat dasselbe Skript gestartet und einen FileNotFoundError bekommen — obwohl die Datei genau dort liegt, wo sie immer lag.
In derselben Woche passierten zwei weitere Dinge. Auf dem Server war APP_DEBUG=false gesetzt, trotzdem zeigten die Logs, dass der Debug-Modus eingeschaltet war. Und jemand hatte temperature: 0 in die Config geschrieben, damit das Modell immer dieselbe Antwort gibt — das Programm lief aber mit 0.7. Keiner dieser Fälle hat einen Fehler ausgelöst.
Keines der drei Probleme liegt an der Logik des Codes. Das Problem ist, wie das Programm die Außenwelt liest — das Dateisystem, Config-Dateien und Umgebungsvariablen. In diesem Kapitel machen wir dieses Lesen verlässlich: Pfade mit pathlib, verschachtelte Config mit JSON und YAML und Geheimnisse mit .env — jeweils mit Prüfung, damit eine falsche Config gleich am Start stoppt, statt still weiterzulaufen.
Am Ende dieses Kapitels kannst du
- Pfade mit
pathlib.Pathbauen, die auf jedem Betriebssystem funktionieren, und dabei/statt verketteteros.path-Strings verwenden - Dateien vom eigenen Speicherort des Skripts aus finden, unabhängig davon, wo das Terminal gerade steht
- Verschachtelte JSON- und YAML-Config lesen und die Stellen erkennen, an denen
yaml.safe_loadTypen still verändert - Werte aus Umgebungsvariablen und einer
.env-Datei lesen, in den richtigen Typ umwandeln und prüfen - Einen Config-Loader schreiben, der mit einer klaren Meldung stoppt, wenn ein Wert falsch ist oder fehlt
Was du vorher brauchst: pyproject.toml, Lockfiles und das src/-Layout — besonders uv add und uv run.
pathlib: ein Pfad ist ein Objekt, kein String
In älterem Python-Code war ein Pfad ein String, der mit + "/" + oder os.path.join zusammengeklebt wurde. Die Klasse Path aus dem Modul pathlib behandelt einen Pfad als Objekt, dessen Teile man abfragen kann:
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 ist / keine Division — bei einem Path fügt es Pfadteile zusammen und wählt den Trenner, den das Betriebssystem verlangt. Die Ausgabe wird mit .as_posix() ausgegeben, damit sie auf jedem Rechner gleich aussieht; ein einfaches print(config_file) zeigt unter Windows config\models\chat.yaml und unter macOS und Linux config/models/chat.yaml.
Dass das einen Unterschied macht, zeigt der Vergleich mit zusammengeklebten Strings:
from pathlib import Path
base = "config"
print(base + "/" + "app.json")
print(Path(base) / "app.json")
print(repr(Path(base) / "app.json"))Unter Windows gibt das aus:
config/app.json
config\app.json
WindowsPath('config/app.json')Die erste Zeile ist ein von Hand gebauter String. Windows akzeptiert / meist, aber sobald solche Pfade an andere Werkzeuge gehen oder als Strings verglichen werden, führen gemischte Trenner zu Verwirrung. Path baut jeden Pfad nach den Regeln des eigenen Systems. Unter macOS oder Linux zeigt die zweite Zeile config/app.json und die dritte PosixPath('config/app.json').
/funktioniert nur, wenn mindestens eine Seite einPathist. Bei"config" / "app.json"sind beide Seiten Strings; Python liest das als Division und wirft einenTypeError. Mach den linken Teil zu einemPath, dann können die übrigen Teile Strings bleiben.
Ein paar weitere Methoden für den Alltag, die die Beispiele unten verwenden:
path.exists(),path.is_file(),path.is_dir()— ob der Pfad existiert und um was für eine Art Ding es sich handeltpath.read_text(encoding="utf-8")undpath.write_text(text, encoding="utf-8")— eine ganze Datei lesen oder schreiben, ohneopenpath.mkdir(parents=True, exist_ok=True)— einen Ordner anlegen, auch die dazwischenliegenden, ohne Fehler, falls er schon existiertfolder.glob("*.txt")— jede Datei in einem Ordner, die zum Muster passt;rglobsucht auch in Unterordnernpath.resolve()— macht aus einem relativen einen absoluten Pfad
Schreib jedes Mal encoding="utf-8". Unter Windows ist Pythons Standard-Encoding oft nicht UTF-8. Eine Prompt-Datei mit bengalischem Text oder einem Emoji lässt sich auf dem Mac problemlos lesen, auf Windows aber ergibt sie verstümmelte Zeichen oder einen UnicodeDecodeError.
Relativ zu was?
Das Skript vom Anfang des Kapitels sieht so aus:
from pathlib import Path
config_path = Path("config/app.json")
print(config_path.read_text(encoding="utf-8"))Aus dem Projektstamm gestartet, mit python scripts/show_config.py, gibt es die Config aus. Wechselst du aber in scripts/ und führst python show_config.py aus:
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'(Auf deinem Rechner zeigt der Traceback die vollständigen Pfade; hier sind sie gekürzt.) Ein relativer Pfad wird immer ab dem aktuellen Arbeitsverzeichnis gesucht — dort, wo das Terminal gerade steht —, nicht ab dem Ort, an dem die Skriptdatei liegt. In scripts/ gibt es kein config/app.json, deshalb wird die Datei nicht gefunden.
Im Produktivbetrieb kommt das noch häufiger vor: Du weißt nicht immer, aus welchem Ordner ein Cronjob, ein Service-Manager oder ein Container dein Programm startet. Die Lösung ist, den Pfad am Speicherort des Skripts festzumachen. In jedem Modul enthält __file__ den Pfad dieser Datei:
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}Jetzt gibt es dieselbe Zeile aus, egal ob du aus dem Projektstamm, aus scripts/ oder mit vollem Pfad von irgendwo anders auf der Festplatte startest. Schritt für Schritt: Path(__file__) ist show_config.py; .resolve() macht ihn absolut; das erste .parent ist scripts/; das zweite .parent ist der Projektstamm.
Lass.resolve()nicht weg. In manchen Situationen ist__file__ein relativer Pfad, und dann hängt.parent.parentwieder vom aktuellen Ordner ab — genau das Problem, das du vermeiden wolltest.
JSON: verschachtelte Config und die Null-Falle
Config in KI-Projekten ist fast immer verschachtelt — App-Einstellungen, Modelleinstellungen und Retrieval-Einstellungen stehen in eigenen Gruppen. json.loads macht aus einem JSON-String verschachtelte Dictionarys (aus einer Datei: 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-false wird zu Pythons False, und Zahlen bleiben Zahlen — JSON behält die Typen. Schau dir aber die letzten beiden Zeilen an. Die Config sagt eindeutig "temperature": 0, trotzdem hat der erste Ansatz 0.7 ergeben.
x or default bedeutet „x, falls x wahr ist, sonst default“. Und in Python sind 0, 0.0, "", [] und {} alle falsy. or kann also nicht unterscheiden zwischen „kein Wert“ und „ein Wert, und der ist null“. Eine Temperatur von 0 sorgt dafür, dass das Modell jedes Mal seine wahrscheinlichste Antwort gibt — genau das willst du für Tests und Evaluierungen. or macht daraus still 0.7, und du siehst nur Antworten, die sich von Lauf zu Lauf unterscheiden.
Verwende.get(key, default)für Standardwerte — der Default greift nur, wenn der Schlüssel wirklich fehlt.orist nur richtig, wenn du auch die Null und leere Werte bewusst verwerfen willst.
YAML: einfach für Menschen, aber es rät die Typen
Viele Teams schreiben Config in YAML, weil es Kommentare erlaubt und weniger Klammern und Anführungszeichen braucht. Python bringt keinen YAML-Leser mit; ergänze ihn im Projekt:
uv add pyyamlDas Paket heißt pyyaml, du importierst es aber als import yaml. Jetzt eine Config lesen:
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 als bei JSON musst du die Typen in YAML nicht ausschreiben — der Parser rät. Meistens liegt er richtig, aber an zwei Stellen liegt er still daneben. yes, no, on und off werden zu Booleans — in der Datei stand cache: off, und du bekommst False. Und 3.10 wird als Dezimalzahl mit dem Wert 3.1 gelesen; als Versionsnummer ist das etwas völlig anderes. Das in Anführungszeichen stehende "3.10" bleibt ein String.
Die Regel ist einfach: Jeder Wert, der wie eine Zahl oder ein Ja/Nein aussieht, aber eigentlich Text ist — Versionsnummern, IDs, Postleitzahlen —, kommt in YAML in Anführungszeichen.
Und verwende immer yaml.safe_load. Das yaml.load(text) aus alten Tutorials schlägt inzwischen direkt fehl mit TypeError: load() missing 1 required positional argument: 'Loader'. Diese Änderung ist bewusst: Manche Loader können aus Anweisungen in der YAML-Datei beliebige Python-Objekte erzeugen. Wenn du also eine nicht vertrauenswürdige YAML-Datei liest, führst du fremden Code aus. safe_load baut nur einfache Typen — Dictionarys, Listen, Strings, Zahlen, Booleans und None.
Umgebungsvariablen: alles ist ein String
API-Schlüssel, Datenbankadressen, das zu verwendende Modell — Werte, die sich von Rechner zu Rechner unterscheiden oder geheim bleiben müssen, gehören in Umgebungsvariablen, nicht in den Code oder in committete Config. Python liest sie mit os.environ oder os.getenv. Für das Beispiel werden zwei Variablen im Code selbst gesetzt; in der echten Arbeit kommen sie aus der Shell, dem Server oder dem 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
30Umgebungsvariablen sind immer Strings. "false" ist ein nicht leerer String mit fünf Zeichen, also behandelt if debug: ihn als wahr — genau so lief der Server vom Anfang des Kapitels im Debug-Modus, obwohl "false" gesetzt war. Ebenso ist "8000" + "1" eine Verkettung, keine Addition. Kein Fehler, nur ein Programm, das das Falsche tut.
Wandle deshalb jede Umgebungsvariable sofort beim Lesen in den passenden Typ um. Für Zahlen reicht int() oder float() — sie werfen selbst einen ValueError, wenn die Eingabe ungültig ist. Für Booleans hat Python keine eingebaute Umwandlung (bool("false") ist True), also schreibst du selbst eine:
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'Die letzte Zeile ist der springende Punkt dieser Funktion. Ein falsch geschriebenes "flase" wird nicht still zu True oder False — das Programm stoppt gleich am Start und sagt, welchen Wert es nicht verstehen konnte.
Merk dir auch den Unterschied zwischen os.environ["NAME"] und os.getenv("NAME"). Das erste wirft KeyError: 'LLM_API_KEY', wenn die Variable fehlt; das zweite gibt still None zurück. Das kann Minuten später als 401 Unauthorized aus einem API-Aufruf zurückkommen — dann ist die Ursache viel schwerer zu finden. Prüfe jeden Wert, ohne den das Programm nicht laufen kann, ganz am Anfang.
Die .env-Datei: eine Umgebung für deinen eigenen Rechner
Zehn Variablen jedes Mal im Terminal zu setzen, wenn du es öffnest, ist unpraktisch. Üblich ist eine .env-Datei im Projektstamm und das Paket python-dotenv, das die Datei liest und die Werte in os.environ schreibt:
uv add python-dotenvAuch hier unterscheiden sich Paketname und Importname: Das Paket heißt python-dotenv, importiert wird dotenv. Eine .env-Datei:
# Local secrets. Never commit this file.
LLM_API_KEY=sk-test-placeholder-1234
LLM_MODEL=chat-small
APP_DEBUG=true(sk-test-placeholder-1234 ist ein erfundener Wert; schreib nie einen echten Schlüssel in ein Dokument, einen Screenshot oder einen Commit.) Der Code zum Lesen, wieder mit dem Pfad vom Speicherort des Skripts aus:
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...Jetzt dasselbe Programm, aber mit bereits in der Shell gesetztem LLM_MODEL (macOS/Linux oder Git Bash):
LLM_MODEL=chat-large uv run main.pyTrue
chat-large
true
sk-test...load_dotenv ersetzt Variablen nicht, die schon gesetzt sind. Das ist Absicht und wichtig: .env ist nur eine Erleichterung für deinen eigenen Rechner, und die echte Umgebung auf einem Server oder in einem Container setzt sich immer durch. (load_dotenv(path, override=True) kehrt diese Reihenfolge um, aber es gibt fast nie einen Grund dafür.)
Beachte außerdem, dass load_dotenv bei einer fehlenden Datei nicht abbricht — es gibt nur False zurück. Nimm also nie an, dass „die .env gelesen wurde“; prüfe die Werte, die du brauchst, separat, wie im nächsten Beispiel.
Zwei Regeln, die jedes professionelle Repository befolgt:
.envwird nie committet. Die Datei gehört in die.gitignore. Ein Schlüssel, der einmal in die Git-Historie gelangt ist, bleibt dort, selbst wenn die Datei später gelöscht wird; die einzige sichere Reaktion ist, den Schlüssel zu widerrufen und einen neuen auszustellen..env.examplewird committet. Sie enthält dieselben Variablennamen mit leeren oder harmlosen Werten. Ein neues Teammitglied kopiert sie nach.envund weiß genau, welche Werte nötig sind.
Ein vollständiges Beispiel
Jetzt alles zusammen: ein Config-Loader für den Support-Bot. Das Projekt wurde mit uv init --app --python 3.12 support-bot angelegt, danach uv add pyyaml python-dotenv. Die Struktur:
support-bot/
├── .env
├── .env.example
├── config/
│ └── app.yaml
├── prompts/
│ ├── summarise.txt
│ └── system.txt
├── pyproject.toml
├── settings.py
└── uv.lockconfig/app.yaml — committet, ohne Geheimnisse darin:
app:
name: support-bot
debug: false
llm:
model: chat-small
temperature: 0.2
max_tokens: 512
retrieval:
top_k: 4.env — nicht committet:
# Local values for this machine. Never commit this file.
LLM_API_KEY=sk-test-placeholder-1234
APP_DEBUG=true.env.example — committet:
# Copy to .env and fill in. This file is committed; .env is not.
LLM_API_KEY=
APP_DEBUG=falseprompts/system.txt enthält eine Zeile, You are a support assistant. Answer only from the provided context., und prompts/summarise.txt enthält Summarise the ticket below in two sentences.
Und 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 Läufe — normal, mit zwei in der Shell geänderten Werten, mit zur Seite geschobener .env und mit einer unmöglichen Temperatur:
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(Vor dem dritten Lauf wurde die .env-Datei umbenannt, und sie wurde vor dem vierten zurückgelegt.) Die letzten beiden Läufe enden mit dem Exit-Code 1, die ersten beiden mit 0 — so kann ein Deploy-Skript oder eine CI leicht erkennen, dass die Config falsch ist.
Die Entscheidungen hinter diesem Loader:
Jeder Pfad kommt von PROJECT_ROOT. Path(__file__).resolve().parent wird einmal berechnet, und jeder andere Pfad baut darauf auf. Aus welchem Ordner das Programm auch gestartet wird — es liest immer dieselben Dateien.
Eine klare Rangfolge. Zuerst die YAML-Standardwerte, darüber die .env, und darüber die echte Umgebung der Shell oder des Servers. Beim zweiten Lauf haben sich Modell und Temperatur geändert, ohne dass eine einzige Datei bearbeitet wurde — so wird Config im Produktivbetrieb an einer einzigen Stelle geändert.
temperature 0.0 bleibt erhalten. os.getenv("LLM_TEMPERATURE", ...) fällt nur dann auf den YAML-Wert zurück, wenn die Variable wirklich fehlt — ein or gibt es nirgends. Ebenso prüft debug mit "APP_DEBUG" in os.environ, nicht, ob der Wert wahr ist.
Typumwandlung und Bereichsprüfungen an einer Stelle. float(), int(), parse_bool() und die Prüfung 0 <= temperature <= 2 liegen alle in load_settings. Der Rest des Programms kann davon ausgehen, dass settings["temperature"] eine gültige Zahl ist.
Eine eigene ConfigError. Jedes Config-Problem erscheint als dieselbe Fehlerart, und der __main__-Block macht daraus eine klare Meldung in einer Zeile — welchen Wert du korrigieren musst —, statt eines Tracebacks.
Geheimnisse werden nie vollständig ausgegeben. mask zeigt nur die ersten drei Zeichen, damit du erkennst, welche Art Schlüssel geladen wurde; der Schlüssel selbst erreicht aber nie die Logs. Auch die Länge der Maske ist fest, sodass man aus ihr nichts über die Länge des Schlüssels erfahren kann.
Prompt-Dateien über glob. Lege eine neue .txt in prompts/ ab, und sie erscheint in settings["prompts"], ohne dass du den Code anfasst. sorted() ist da, weil die Reihenfolge, in der glob die Dateien liefert, vom Betriebssystem abhängt.
Eine Einschränkung: load_settings gibt ein einfaches Dictionary zurück. Ein Tippfehler wie settings["tempreature"] wird daher erst zur Laufzeit als KeyError bemerkt, und jede neue Einstellung braucht Umwandlung und Prüfung von Hand. Ein Werkzeug, das das deklarativ erledigt, kommt später im Kurs; die Prinzipien hier — an einer Stelle laden, die richtigen Typen, am Start stoppen — bleiben mit ihm dieselben.
Wenn es schiefgeht
FileNotFoundError: [Errno 2] No such file or directory: 'config\\app.json' Der Pfad ist relativ, und das Programm wurde aus einem anderen Ordner gestartet. Baue den Pfad aus Path(__file__).resolve().parent. Um zu sehen, von welchem Ordner die Suche startet, gib Path.cwd() aus.
TypeError: unsupported operand type(s) for /: 'str' and 'str' "config" / "app.json" — Strings auf beiden Seiten. Mach den linken Teil zu einem Path: Path("config") / "app.json".
json.decoder.JSONDecodeError: Expecting property name enclosed in double quotes: line 1 column 36 (char 35) Das JSON hat ein überflüssiges Komma nach dem letzten Eintrag ({"top_k": 4,}) oder einen Schlüssel in einfachen Anführungszeichen. Python-Dictionarys akzeptieren das; JSON nicht. Die Spaltennummer in der Meldung zeigt genau, wo der Parser hängen geblieben ist.
TypeError: load() missing 1 required positional argument: 'Loader' Alter Code, der yaml.load(text) aufruft. Schreib stattdessen yaml.safe_load(text).
KeyError: 'LLM_API_KEY' Der Code hat os.environ["LLM_API_KEY"] gelesen, und die Variable ist nicht gesetzt. Prüfe, ob sich .env im Projektstamm befindet, ob load_dotenv den richtigen Pfad bekommen hat und ob der Name überall gleich geschrieben ist. Für eine klare Meldung bei fehlenden Werten nimm eine Funktion wie require_env von oben.
ModuleNotFoundError: No module named 'dotenv' python-dotenv wurde nie zum Projekt hinzugefügt, oder das Programm wurde ohne uv run gestartet, sodass die Umgebung des Projekts nicht verwendet wurde. Führe uv add python-dotenv aus und starte das Programm mit uv run.
Schritt 4 von 6 — Vorhersagen
Prüfe dein Verständnis
Auf dem Server ist APP_DEBUG=false gesetzt (hier gezeigt, indem es im Code gesetzt wird). Was wird ausgegeben?
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'`
Für einen Evaluierungslauf setzt die Config temperature auf 0, damit das Modell jedes Mal gleich antwortet. Doch die Antworten unterscheiden sich bei jedem Lauf, und es gibt keinen Fehler. Wo steckt der Fehler?
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` ist falsy, also verwirft `or` sie und nimmt `0.7`; richtig wäre `config["llm"].get("temperature", 0.7)`
- B`.get("temperature")` funktioniert nicht auf verschachtelten Dictionarys; es muss `config["temperature"]` heißen
- CEinfache Anführungszeichen innerhalb des f-Strings ändern den Wert von `temperature`
- DDie `0` aus dem JSON ist eigentlich der String `"0"`, deshalb schlägt der Vergleich fehl
Dieser Code liegt in scripts/show_config.py, und die Config liegt unter config/app.json im Projektstamm. Aus dem Stamm funktioniert es, aus scripts/ gibt es FileNotFoundError. Was ist die beste Lösung?
from pathlib import Path
config_path = Path("config/app.json")
print(config_path.read_text(encoding="utf-8"))- ADen Pfad als `"../config/app.json"` schreiben
- BZusätzlich eine Kopie von `config/app.json` im Ordner `scripts/` aufbewahren
- C`Path(__file__).resolve().parent.parent / "config" / "app.json"` — den Pfad vom eigenen Speicherort des Skripts aus bauen
- DAllen sagen, dass das Skript nur aus dem Projektstamm gestartet werden darf
Spielen ist Teil von Pro
+30 XP warten
Spiele dieses Kapitel mit Pro
Lesen ist kostenlos, und das ganze Kapitel ist offen. Die ersten 5 Kapitel jedes kostenlosen Kurses kannst du kostenlos spielen. Ab Kapitel 6 gehören Fragen beantworten, XP sammeln und deine Serie ausbauen zu Pro.
Spielen mit Pro freischaltenJetzt bist du dran
Schreib einen Config-Loader, der eine umgebungsspezifische Config über eine Basis-Config legt. Lege ein Projekt mit uv init --app --python 3.12 config-lab an, führe uv add python-dotenv aus und erstelle dann diese Dateien:
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: eine Zeile,APP_ENV=staging
Schreib main.py so, dass:
- Jeder Pfad aus
Path(__file__).resolve().parentgebaut wird, sodass das Programm aus jedem Ordner das gleiche Ergebnis liefert APP_ENVaus der Umgebung oder der.envkommt, mit dem Standardwertdev; für eine Umgebung ohne Datei (etwaconfig/prod.json) gibt das Programmconfig error: no config/prod.jsonaus und stoppt mit dem Exit-Code1- Die beiden Configs tief zusammengeführt werden: verschachtelte Dictionarys werden Schlüssel für Schlüssel zusammengeführt statt komplett ersetzt, sodass das
modelausbase.jsonerhalten bleibt, obwohlstaging.jsonkeines hat - Eine
temperaturevon0als0erhalten bleibt — ohneor
Wenn du uv run main.py aus dem Projektstamm ausführst, sollte genau das herauskommen:
env staging
model chat-small
temperature 0
top_k 8Danach teste zwei Dinge. Erstens: Führe APP_ENV=prod uv run main.py aus und prüfe, dass das Programm mit einer klaren Meldung stoppt. Zweitens: Probiere statt des tiefen Merges ein einfaches {**base, **overlay} und gib config["llm"] aus — wo ist model geblieben, und warum hat die Merge-Zeile selbst keinen Fehler ausgelöst?
Schritt 6 von 6
Herausforderung — das Kapitel-Quiz
Zehn Fragen von leicht bis schwer. Die letzten sind absichtlich knifflig.
Mit Pro spielen