pathlib, configurazione JSON/YAML e variabili d'ambiente
Percorsi che funzionano da qualsiasi cartella con pathlib, configurazione JSON e YAML annidata, .env e variabili d'ambiente — e un loader di configurazione che si ferma all'avvio quando un valore è sbagliato.
Come vuoi imparare?
Il problema che stiamo risolvendo
Un progetto di chatbot di supporto ha uno script piccolo, scripts/show_config.py, che legge config/app.json e mostra il nome del modello e la temperatura. Il suo autore sta sempre nella radice del progetto ed esegue python scripts/show_config.py, e funziona. Un giorno un collega è entrato nella cartella scripts/, ha eseguito lo stesso script e ha ottenuto un FileNotFoundError — con il file esattamente dove è sempre stato.
Nella stessa settimana sono successe altre due cose. Sul server era impostato APP_DEBUG=false, eppure nei log risultava attiva la modalità debug. E qualcuno ha scritto temperature: 0 nella configurazione perché il modello desse sempre la stessa risposta — ma il programma girava con 0.7. Nessuno di questi casi ha sollevato un errore.
Nessuno dei tre è un problema della logica del codice. Il problema è il modo in cui il programma legge il mondo esterno — il file system, i file di configurazione, le variabili d'ambiente. In questo capitolo rendiamo affidabile questa lettura: percorsi con pathlib, configurazione annidata con JSON e YAML, e segreti con .env — ognuno con una validazione, così una configurazione sbagliata si ferma all'avvio invece di girare in silenzio.
Alla fine di questo capitolo sarai in grado di
- Costruire percorsi che funzionano su qualsiasi sistema operativo con
pathlib.Path, usando/invece di unire stringhe conos.path - Trovare i file a partire dalla posizione dello script, indipendentemente da dove si trova il terminale
- Leggere configurazioni JSON e YAML annidate, e riconoscere i modi in cui
yaml.safe_loadcambia i tipi in silenzio - Leggere valori da variabili d'ambiente e da un file
.env, convertirli nel tipo giusto e validarli - Scrivere un loader di configurazione che si ferma con un messaggio chiaro quando un valore è sbagliato o manca
Prerequisiti: pyproject.toml, lockfile e layout src/ — soprattutto uv add e uv run.
pathlib: un percorso è un oggetto, non una stringa
Nel vecchio codice Python un percorso era una stringa, incollata con + "/" + o con os.path.join. La classe Path del modulo pathlib tratta un percorso come un oggetto di cui puoi chiedere le parti:
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')Qui / non è una divisione — usato con un Path, unisce le parti del percorso e sceglie il separatore giusto per il sistema operativo. L'output viene stampato con .as_posix() così appare uguale su ogni macchina; un semplice print(config_file) mostra config\models\chat.yaml su Windows e config/models/chat.yaml su macOS e Linux.
Unire stringhe rende la differenza visibile:
from pathlib import Path
base = "config"
print(base + "/" + "app.json")
print(Path(base) / "app.json")
print(repr(Path(base) / "app.json"))Su Windows questo stampa:
config/app.json
config\app.json
WindowsPath('config/app.json')La prima riga è una stringa costruita a mano. Windows di solito accetta /, ma appena passi questi percorsi ad altri strumenti o li confronti come stringhe, due tipi di separatore mescolati creano confusione. Path costruisce ogni percorso con le regole del proprio sistema. Su macOS o Linux la seconda riga mostra config/app.json e la terza PosixPath('config/app.json').
/funziona solo quando almeno un lato è unPath. In"config" / "app.json"entrambi i lati sono stringhe, quindi Python la legge come divisione e solleva unTypeError. RendiPathla parte più a sinistra e il resto può restare stringa.
Alcuni altri metodi per il lavoro di tutti i giorni, usati negli esempi qui sotto:
path.exists(),path.is_file(),path.is_dir()— se il percorso esiste, e che tipo di oggetto èpath.read_text(encoding="utf-8")epath.write_text(text, encoding="utf-8")— leggere o scrivere un intero file senzaopenpath.mkdir(parents=True, exist_ok=True)— creare una cartella, incluse quelle intermedie, senza errore se esiste giàfolder.glob("*.txt")— ogni file di una cartella che corrisponde a un pattern;rglobcerca anche nelle sottocartellepath.resolve()— trasforma un percorso relativo in uno assoluto
Scrivi sempre encoding="utf-8". Su Windows la codifica predefinita di Python spesso non è UTF-8, quindi un file di prompt che contiene testo in bengalese o un'emoji si legge bene su un Mac e dà caratteri illeggibili o un UnicodeDecodeError su Windows.
Rispetto a cosa?
Lo script dell'inizio del capitolo ha questo aspetto:
from pathlib import Path
config_path = Path("config/app.json")
print(config_path.read_text(encoding="utf-8"))Dalla radice del progetto, python scripts/show_config.py stampa la configurazione. Ma entra in scripts/ ed esegui 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'(Sulla tua macchina il traceback mostra i percorsi completi; qui sono stati accorciati.) Un percorso relativo viene sempre cercato a partire dalla cartella di lavoro corrente — dove si trova il terminale — e non da dove sta il file dello script. Dentro scripts/ non c'è nessun config/app.json, quindi il file non viene trovato.
In produzione succede ancora di più: non sempre sai da quale cartella un cron job, un gestore di servizi o un container avvierà il tuo programma. La soluzione è legare il percorso alla posizione dello script. In ogni modulo, __file__ contiene il percorso di quel file:
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}Ora stampa la stessa riga sia che tu lo esegua dalla radice del progetto, da scripts/, sia da un punto qualsiasi del disco con un percorso completo. Passo per passo: Path(__file__) è show_config.py; .resolve() lo rende assoluto; il primo .parent è scripts/; il secondo .parent è la radice del progetto.
Non togliere.resolve(). In alcune situazioni__file__è un percorso relativo, e allora.parent.parentdipende di nuovo dalla cartella corrente — proprio il problema che stavi cercando di evitare.
JSON: configurazione annidata e la trappola dello zero
La configurazione nei progetti di AI è quasi sempre annidata — impostazioni dell'app, del modello e del retrieval in gruppi separati. json.loads trasforma una stringa JSON in dizionari annidati (da un file, 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
0Il false di JSON diventa False di Python e i numeri restano numeri — JSON conserva i tipi. Ma guarda le ultime due righe. La configurazione dice chiaramente "temperature": 0, eppure il primo approccio ha dato 0.7.
x or default significa «x se x è veritiero, altrimenti default». E in Python 0, 0.0, "", [] e {} sono tutti considerati falsi. Quindi or non riesce a distinguere «nessun valore» da «un valore, ed è zero». Una temperatura di 0 fa sì che il modello dia sempre la risposta più probabile, che è ciò che vuoi per test e valutazioni; or la trasforma in silenzio in 0.7, e tu vedi solo risposte che cambiano da un'esecuzione all'altra.
Usa.get(key, default)per i valori predefiniti — usa il default solo quando la chiave è davvero assente.orva bene solo quando vuoi davvero scartare anche zero e valori vuoti.
YAML: facile per le persone, ma indovina i tipi
Molti team scrivono la configurazione in YAML, perché permette i commenti e ha meno parentesi e virgolette. Python non include un lettore YAML; aggiungilo al progetto:
uv add pyyamlIl pacchetto si chiama pyyaml, ma lo importi come import yaml. Ora leggi una configurazione:
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 differenza di JSON, YAML non ti chiede di specificare i tipi — il parser indovina. Nella maggior parte dei casi l'ipotesi è giusta, ma in due punti è silenziosamente sbagliata. yes, no, on e off diventano booleani — il file diceva cache: off e tu hai ottenuto False. E 3.10 viene letto come numero decimale il cui valore è 3.1; come numero di versione è tutt'altra cosa. La stringa tra virgolette "3.10" resta una stringa.
La regola è semplice: ogni valore che sembra un numero o un sì/no ma in realtà è testo — numeri di versione, ID, codici postali — va messo tra virgolette in YAML.
E usa sempre yaml.safe_load. Il yaml.load(text) che vedi nei tutorial vecchi ora fallisce direttamente con TypeError: load() missing 1 required positional argument: 'Loader'. Il cambiamento è voluto: alcuni loader possono costruire oggetti Python arbitrari a partire da istruzioni dentro il file YAML, quindi leggere un file YAML non fidato significa eseguire codice di altri. safe_load costruisce solo tipi semplici — dizionari, liste, stringhe, numeri, booleani, None.
Variabili d'ambiente: tutto è una stringa
Chiavi API, indirizzi di database, quale modello usare — i valori che cambiano da una macchina all'altra o devono restare segreti vivono nelle variabili d'ambiente, non nel codice né in una configurazione committata. Python le legge con os.environ o os.getenv. Per l'esempio, due variabili sono impostate dentro il codice stesso; nel lavoro reale arrivano dalla shell, dal server o dal 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
30Le variabili d'ambiente sono sempre stringhe. "false" è una stringa non vuota di cinque caratteri, quindi if debug: la considera vera — esattamente così il server dell'inizio del capitolo girava in modalità debug con "false" impostato. Allo stesso modo, "8000" + "1" è una concatenazione, non un'addizione. Nessun errore, solo un programma che fa la cosa sbagliata.
Quindi converti ogni variabile d'ambiente nel tipo giusto appena la leggi. Per i numeri bastano int() o float() — sollevano da soli ValueError con un input sbagliato. Per i booleani Python non ha una conversione integrata (bool("false") è True), quindi la scrivi tu:
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'L'ultima riga è il motivo per cui esiste questa funzione. Un "flase" scritto male non diventa in silenzio True o False — il programma si ferma all'avvio e dice quale valore non ha capito.
Tieni presente anche la differenza tra os.environ["NAME"] e os.getenv("NAME"). Il primo solleva KeyError: 'LLM_API_KEY' quando la variabile manca; il secondo restituisce in silenzio None, che può comparire qualche minuto dopo come 401 Unauthorized da una chiamata API — e a quel punto la causa è molto più difficile da trovare. Controlla all'inizio ogni valore senza il quale il programma non può girare.
Il file .env: un ambiente per la tua macchina
Impostare dieci variabili ogni volta che apri un terminale non è realistico. La risposta comune è un file .env nella radice del progetto e il pacchetto python-dotenv, che legge quel file e mette i valori in os.environ:
uv add python-dotenvAnche qui il nome del pacchetto e quello dell'import sono diversi: il pacchetto è python-dotenv, l'import è dotenv. Un file .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 è un valore inventato; non mettere mai una chiave reale in un documento, uno screenshot o un commit.) Il codice per leggerlo, costruendo di nuovo il percorso dalla posizione dello 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...Ora lo stesso programma, ma con LLM_MODEL già impostato nella shell (macOS/Linux o Git Bash):
LLM_MODEL=chat-large uv run main.pyTrue
chat-large
true
sk-test...load_dotenv non sostituisce le variabili già impostate. È voluto e importante: .env è solo una comodità per la tua macchina, e l'ambiente reale impostato su un server o in un container vince sempre. (load_dotenv(path, override=True) inverte questo ordine, ma quasi mai c'è un motivo per volerlo.)
Nota anche che load_dotenv non solleva un errore quando il file manca — restituisce solo False. Quindi non dare mai per scontato che «.env sia stato letto»; controlla separatamente i valori di cui hai bisogno, come fa il prossimo esempio.
Due regole che ogni repository professionale segue:
.envnon viene mai committato. Va in.gitignore. Una volta che una chiave è entrata nella storia di Git, ci resta anche dopo che il file è stato cancellato; l'unica risposta sicura è revocare la chiave e crearne una nuova..env.exampleviene committato. Ha gli stessi nomi di variabili con valori vuoti o innocui. Un nuovo collega lo copia in.enve sa esattamente quali valori servono.
Un esempio completo
Ora mettiamo insieme tutto: un loader di configurazione per il chatbot di supporto. Il progetto è stato creato con uv init --app --python 3.12 support-bot, poi uv add pyyaml python-dotenv. La struttura:
support-bot/
├── .env
├── .env.example
├── config/
│ └── app.yaml
├── prompts/
│ ├── summarise.txt
│ └── system.txt
├── pyproject.toml
├── settings.py
└── uv.lockconfig/app.yaml — committato, nulla di segreto dentro:
app:
name: support-bot
debug: false
llm:
model: chat-small
temperature: 0.2
max_tokens: 512
retrieval:
top_k: 4.env — non committato:
# Local values for this machine. Never commit this file.
LLM_API_KEY=sk-test-placeholder-1234
APP_DEBUG=true.env.example — committato:
# Copy to .env and fill in. This file is committed; .env is not.
LLM_API_KEY=
APP_DEBUG=falseprompts/system.txt contiene una sola riga, You are a support assistant. Answer only from the provided context., e prompts/summarise.txt contiene Summarise the ticket below in two sentences.
E 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'])}")Quattro esecuzioni — normale, con due valori cambiati dalla shell, con .env spostato da parte, e con una temperatura impossibile:
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(Prima della terza esecuzione il file .env è stato rinominato per toglierlo di mezzo, e rimesso prima della quarta.) Le ultime due esecuzioni terminano con codice di uscita 1 e le prime due con 0, quindi uno script di deploy o la CI può capire facilmente che la configurazione è sbagliata.
Le scelte che stanno dietro questo loader:
Ogni percorso parte da PROJECT_ROOT. Path(__file__).resolve().parent viene calcolato una volta sola e ogni altro percorso si costruisce su di esso. Qualunque sia la cartella da cui il programma viene avviato, legge sempre gli stessi file.
Un ordine di precedenza esplicito. Prima i valori predefiniti in YAML, sopra di essi .env, e sopra tutto l'ambiente reale della shell o del server. Nella seconda esecuzione modello e temperatura sono cambiati senza modificare un solo file — ed è così che in produzione la configurazione si cambia in un unico posto.
temperature 0.0 sopravvive. os.getenv("LLM_TEMPERATURE", ...) ricade sul valore YAML solo quando la variabile è davvero assente — da nessuna parte c'è un or. Allo stesso modo debug controlla "APP_DEBUG" in os.environ, non se il valore è veritiero.
Conversione dei tipi e controlli di intervallo in un unico posto. float(), int(), parse_bool() e il controllo 0 <= temperature <= 2 vivono tutti in load_settings. Il resto del programma può dare per scontato che settings["temperature"] sia un numero valido.
Un ConfigError dedicato. Ogni problema di configurazione viene segnalato con un solo tipo di errore, e il blocco __main__ lo trasforma in un messaggio chiaro su una riga — quale valore correggere, invece di un traceback.
I segreti non vengono mai stampati per intero. mask mostra i primi tre caratteri, così puoi capire quale tipo di chiave è stata caricata, ma la chiave non arriva mai ai log. Anche la lunghezza della maschera è fissa, quindi dalla lunghezza della chiave non si può ricavare nulla.
File di prompt tramite glob. Metti un nuovo .txt in prompts/ e comparirà in settings["prompts"] senza toccare il codice. sorted() c'è perché l'ordine in cui glob restituisce i file dipende dal sistema operativo.
Un limite: load_settings restituisce un semplice dizionario, quindi un refuso come settings["tempreature"] viene catturato solo come KeyError a runtime, e ogni nuova impostazione richiede la sua conversione e il suo controllo scritti a mano. Più avanti nel corso arriverà uno strumento che lo fa in modo dichiarativo; i principi qui — caricare in un solo posto, i tipi giusti, fermarsi all'avvio — restano gli stessi anche con quello.
Quando si rompe
FileNotFoundError: [Errno 2] No such file or directory: 'config\\app.json' Il percorso è relativo e il programma è stato avviato da un'altra cartella. Costruisci il percorso da Path(__file__).resolve().parent. Per vedere da quale cartella parte la ricerca, stampa Path.cwd().
TypeError: unsupported operand type(s) for /: 'str' and 'str' "config" / "app.json" — stringhe su entrambi i lati. Rendi Path la parte più a sinistra: Path("config") / "app.json".
json.decoder.JSONDecodeError: Expecting property name enclosed in double quotes: line 1 column 36 (char 35) Il JSON ha una virgola finale dopo l'ultimo elemento ({"top_k": 4,}), oppure una chiave tra apici singoli. I dizionari Python accettano queste forme; JSON no. Il numero di colonna nel messaggio mostra esattamente dove il parser si è bloccato.
TypeError: load() missing 1 required positional argument: 'Loader' Codice vecchio che chiama yaml.load(text). Scrivi invece yaml.safe_load(text).
KeyError: 'LLM_API_KEY' Il codice ha letto os.environ["LLM_API_KEY"] e la variabile non è impostata. Controlla che .env sia nella radice del progetto, che load_dotenv riceva il percorso giusto e che il nome sia scritto allo stesso modo ovunque. Per dare un messaggio chiaro quando un valore manca, usa una funzione come require_env qui sopra.
ModuleNotFoundError: No module named 'dotenv' python-dotenv non è mai stato aggiunto al progetto, oppure il programma è stato eseguito senza uv run, quindi non è stato usato l'ambiente del progetto. Esegui uv add python-dotenv, e avvia il programma con uv run.
Passo 4 di 6 — Prevedi
Verifica cosa hai capito
Sul server è impostato APP_DEBUG=false (qui lo mostriamo impostandolo nel codice). Cosa viene stampato?
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'`
Per una valutazione, la configurazione imposta temperature a 0 perché il modello risponda sempre allo stesso modo. Ma le risposte cambiano a ogni esecuzione, e non c'è alcun errore. Dov'è il 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` è falso, quindi `or` lo scarta e prende `0.7`; dovrebbe essere `config["llm"].get("temperature", 0.7)`
- B`.get("temperature")` non funziona sui dizionari annidati; deve essere `config["temperature"]`
- CUsare apici singoli dentro la f-string cambia il valore di `temperature`
- DLo `0` che arriva dal JSON è in realtà la stringa `"0"`, quindi il confronto fallisce
Questo codice sta in scripts/show_config.py, e la configurazione è in config/app.json nella radice del progetto. Dalla radice funziona, da scripts/ dà FileNotFoundError. Qual è la correzione migliore?
from pathlib import Path
config_path = Path("config/app.json")
print(config_path.read_text(encoding="utf-8"))- AScrivere il percorso come `"../config/app.json"`
- BTenere una copia di `config/app.json` anche dentro la cartella `scripts/`
- C`Path(__file__).resolve().parent.parent / "config" / "app.json"` — costruire il percorso dalla posizione dello script stesso
- DDire a tutti che lo script deve essere eseguito solo dalla radice del progetto
Giocare fa parte di Pro
+30 XP ti aspettano
Gioca questo capitolo con Pro
Leggere è gratis e l’intero capitolo è aperto. I primi 5 capitoli di ogni corso gratuito si giocano gratis. Dal capitolo 6, rispondere alle domande, guadagnare XP e far crescere la serie fa parte di Pro.
Sblocca il gioco con ProTocca a te
Scrivi un loader di configurazione che sovrappone una configurazione specifica per ambiente a una configurazione di base. Crea un progetto con uv init --app --python 3.12 config-lab, esegui uv add python-dotenv, poi crea questi file:
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 riga,APP_ENV=staging
Scrivi main.py in modo che:
- Ogni percorso sia costruito da
Path(__file__).resolve().parent, così il programma dà lo stesso risultato da qualsiasi cartella APP_ENVvenga dall'ambiente o da.env, con valore predefinitodev; per un ambiente senza file (comeconfig/prod.json) il programma stampiconfig error: no config/prod.jsone si fermi con codice di uscita1- Le due configurazioni vengano unite in profondità: i dizionari annidati si uniscono chiave per chiave invece di essere sostituiti per intero, così il
modeldibase.jsonsopravvive anche sestaging.jsonnon ne ha uno - Una
temperaturedi0resti0— senzaor
Eseguendo uv run main.py dalla radice del progetto dovresti ottenere esattamente questo:
env staging
model chat-small
temperature 0
top_k 8Poi prova due cose. Primo, esegui APP_ENV=prod uv run main.py e verifica che il programma si fermi con un messaggio chiaro. Secondo, prova un semplice {**base, **overlay} al posto del merge profondo e stampa config["llm"] — dove è finito model, e perché la riga di merge stessa non ha sollevato alcun errore?
Passo 6 di 6
Sfida — il quiz del capitolo
Dieci domande dalla più facile alla più difficile. Le ultime sono difficili di proposito.
Gioca con Pro