pathlib, JSON/YAML कॉन्फ़िगरेशन और एनवायरनमेंट वेरिएबल्स
pathlib से किसी भी फ़ोल्डर से काम करने वाले पाथ्स, नेस्टेड JSON और YAML कॉन्फ़िग, .env और एनवायरनमेंट वेरिएबल्स — और गलत मान होने पर तुरंत रुकने वाला कॉन्फ़िग लोडर।
आप कैसे सीखना चाहते हैं?
वह समस्या जिसे हम हल कर रहे हैं
एक सपोर्ट-बॉट प्रोजेक्ट में एक छोटी स्क्रिप्ट है, scripts/show_config.py, जो config/app.json को पढ़कर मॉडल का नाम और टेम्परेचर दिखाती है। इसके लेखक हमेशा प्रोजेक्ट रूट में खड़े होकर python scripts/show_config.py चलाते हैं, और सब कुछ ठीक काम करता है। एक दिन एक सहकर्मी scripts/ फ़ोल्डर के अंदर गया, वही स्क्रिप्ट चलाई, और FileNotFoundError आ गया — जबकि फ़ाइल बिल्कुल वहीं थी जहाँ वह हमेशा से रहती आई है।
उसी हफ़्ते दो और घटनाएँ हुईं। सर्वर पर APP_DEBUG=false सेट किया गया था, फिर भी लॉग्स में डिबग मोड चालू (ON) दिख रहा था। और किसी ने कॉन्फ़िगरेशन में temperature: 0 लिखा था ताकि मॉडल हर बार एक ही निश्चित उत्तर दे — लेकिन प्रोग्राम 0.7 के साथ चल रहा था। इनमें से किसी ने भी कोई एरर नहीं दिया।
इन तीनों में से कोई भी समस्या कोड के लॉजिक की नहीं है। समस्या इस बात में है कि प्रोग्राम बाहरी दुनिया (the outside world) को कैसे पढ़ता है — फ़ाइल सिस्टम, कॉन्फ़िग फ़ाइलें, एनवायरनमेंट वेरिएबल्स। इस अध्याय में हम उस रीडिंग को भरोसेमंद बनाएंगे: pathlib के साथ पाथ्स, JSON और YAML के साथ नेस्टेड कॉन्फ़िगरेशन, और .env के साथ सीक्रेट्स — प्रत्येक में वैलिडेशन के साथ, ताकि ग़लत कॉन्फ़िग चुपचाप चलने के बजाय शुरुआत में ही रुक जाए।
इस अध्याय के अंत में आप कर पाएंगे
pathlib.Pathके साथ ऐसे पाथ्स बनाना जो किसी भी ऑपरेटिंग सिस्टम पर काम करें,os.pathस्ट्रिंग्स जोड़ने के बजाय/का उपयोग करके- टर्मिनल कहाँ खड़ा है इसकी परवाह किए बिना, स्क्रिप्ट के अपने स्थान के आधार पर फ़ाइलें खोजना
- नेस्टेड JSON और YAML कॉन्फ़िग पढ़ना, और उन तरीकों को पहचानना जिनसे
yaml.safe_loadचुपचाप टाइप्स बदल देता है - एनवायरनमेंट वेरिएबल्स और
.envफ़ाइल से मान पढ़ना, उन्हें सही प्रकार में बदलना और उन्हें वैलिडेट करना - एक ऐसा कॉन्फ़िग लोडर लिखना जो कोई मान ग़लत या गायब होने पर स्पष्ट संदेश के साथ रुक जाए
ज़रूरी शर्तें: pyproject.toml, लॉकफाइलें और src/ लेआउट — विशेष रूप से uv add और uv run।
pathlib: पाथ एक ऑब्जेक्ट है, स्ट्रिंग नहीं
पुराने पायथन कोड में पाथ एक स्ट्रिंग होता था, जिसे + "/" + या os.path.join से जोड़ा जाता था। pathlib मॉड्यूल का Path क्लास पाथ को एक ऑब्जेक्ट मानता है जिसके विभिन्न हिस्सों के बारे में आप पूछ सकते हैं:
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')यहाँ / विभाजन (division) नहीं है — Path के साथ उपयोग किए जाने पर, यह पाथ के हिस्सों को जोड़ता है और ऑपरेटिंग सिस्टम के लिए सही सेपरेटर (separator) चुनता है। आउटपुट को .as_posix() के साथ प्रिंट किया गया है ताकि यह हर मशीन पर एक जैसा दिखे; साधारण print(config_file) Windows पर config\models\chat.yaml और macOS व Linux पर config/models/chat.yaml दिखाता है।
स्ट्रिंग्स को मैन्युअल रूप से जोड़ने पर अंतर साफ़ दिखता है:
from pathlib import Path
base = "config"
print(base + "/" + "app.json")
print(Path(base) / "app.json")
print(repr(Path(base) / "app.json"))Windows पर यह प्रिंट करता है:
config/app.json
config\app.json
WindowsPath('config/app.json')पहली पंक्ति हाथ से बनाई गई स्ट्रिंग है। Windows आमतौर पर / स्वीकार कर लेता है, लेकिन एक बार जब आप ऐसे पाथ्स अन्य टूल्स को सौंपते हैं या स्ट्रिंग्स के रूप में तुलना करते हैं, तो दो तरह के सेपरेटर्स आपस में मिलकर भ्रम पैदा करते हैं। Path प्रत्येक पाथ को उसके अपने सिस्टम के नियमों के अनुसार बनाता है। macOS या Linux पर दूसरी पंक्ति config/app.json और तीसरी पंक्ति PosixPath('config/app.json') दिखाती है।
/केवल तभी काम करता है जब कम से कम एक पक्षPathहो।"config" / "app.json"में दोनों पक्ष स्ट्रिंग्स हैं, इसलिए पायथन इसे गणितीय विभाजन समझकरTypeErrorदेता है। सबसे बाएँ हिस्से कोPathबनाएं और बाकी हिस्से स्ट्रिंग्स रह सकते हैं।
दैनिक कार्य के लिए कुछ और उपयोगी मेथड्स, जिनका उपयोग नीचे दिए गए उदाहरणों में किया गया है:
path.exists(),path.is_file(),path.is_dir()— पाथ मौजूद है या नहीं, और वह किस प्रकार की चीज़ हैpath.read_text(encoding="utf-8")औरpath.write_text(text, encoding="utf-8")— बिनाopenके पूरी फ़ाइल को पढ़ना या लिखनाpath.mkdir(parents=True, exist_ok=True)— एक फ़ोल्डर बनाना, बीच के सभी फ़ोल्डर्स सहित, यदि वह पहले से मौजूद हो तो बिना किसी एरर केfolder.glob("*.txt")— फ़ोल्डर में किसी पैटर्न से मेल खाने वाली प्रत्येक फ़ाइल;rglobसबफ़ोल्डर्स में भी खोजता हैpath.resolve()— रिलेटिव पाथ को एब्सोल्यूट (पूर्ण) पाथ में बदलना
हर बार encoding="utf-8" ज़रूर लिखें। Windows पर, पायथन की डिफ़ॉल्ट एन्कोडिंग अक्सर UTF-8 नहीं होती, इसलिए हिंदी टेक्स्ट या इमोजी वाली प्रॉम्प्ट फ़ाइल Mac पर ठीक से पढ़ ली जाती है लेकिन Windows पर विकृत वर्ण या UnicodeDecodeError दे सकती है।
किसके सापेक्ष (Relative to what)?
अध्याय की शुरुआत वाली स्क्रिप्ट कुछ ऐसी दिखती है:
from pathlib import Path
config_path = Path("config/app.json")
print(config_path.read_text(encoding="utf-8"))प्रोजेक्ट रूट से, python scripts/show_config.py कॉन्फ़िग प्रिंट कर देता है। लेकिन scripts/ में कदम रखें और 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'(आपकी मशीन पर ट्रेसबैक पूरे पाथ्स दिखाता है; यहाँ उन्हें संक्षिप्त किया गया है।) रिलेटिव पाथ हमेशा वर्तमान वर्किंग डायरेक्टरी (current working directory) से खोजा जाता है — यानी टर्मिनल जहाँ भी खड़ा है — न कि वहाँ से जहाँ स्क्रिप्ट फ़ाइल स्थित है। scripts/ के अंदर कोई config/app.json नहीं है, इसलिए फ़ाइल नहीं मिली।
प्रोडक्शन में ऐसा और भी ज़्यादा होता है: आपको हमेशा यह पता नहीं होता कि कोई क्रॉन जॉब (cron job), सर्विस मैनेजर या कंटेनर आपके प्रोग्राम को किस फ़ोल्डर से शुरू करेगा। इसका समाधान पाथ को स्क्रिप्ट के अपने स्थान से जोड़ना है। प्रत्येक मॉड्यूल के अंदर, __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}अब यह वही लाइन प्रिंट करता है चाहे आप इसे प्रोजेक्ट रूट से चलाएं, scripts/ से चलाएं, या डिस्क पर कहीं और से पूरे पाथ के साथ चलाएं। चरण दर चरण: Path(__file__) है show_config.py; .resolve() इसे पूर्ण (absolute) बनाता है; पहला .parent है scripts/; दूसरा .parent है प्रोजेक्ट रूट।
कभी भी.resolve()को न छोड़ें। कुछ परिस्थितियों में__file__एक रिलेटिव पाथ होता है, और तब.parent.parentफिर से वर्तमान फ़ोल्डर पर निर्भर हो जाता है — ठीक वही समस्या जिससे आप बचने की कोशिश कर रहे थे।
JSON: नेस्टेड कॉन्फ़िग और शून्य का जाल
एआई प्रोजेक्ट्स में कॉन्फ़िगरेशन लगभग हमेशा नेस्टेड होता है — ऐप सेटिंग्स, मॉडल सेटिंग्स और रिट्रीवल सेटिंग्स अलग-अलग समूहों में। json.loads एक JSON स्ट्रिंग को नेस्टेड डिक्शनरीज़ में बदल देता है (फ़ाइल से, 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 पायथन का False बन जाता है और संख्याएँ संख्याएँ ही रहती हैं — JSON डेटा टाइप्स को सुरक्षित रखता है। लेकिन अंतिम दो पंक्तियों को देखें। कॉन्फ़िग में स्पष्ट रूप से "temperature": 0 लिखा है, फिर भी पहले तरीके ने 0.7 दिया।
x or default का अर्थ है "x यदि x सत्य (truthy) है, अन्यथा default"। और पायथन में, 0, 0.0, "", [] और {} सभी असत्य (falsy) होते हैं। इसलिए or "कोई मान नहीं" और "एक मान है, और वह शून्य है" के बीच अंतर नहीं कर सकता। 0 का टेम्परेचर मॉडल को हर बार उसका सबसे संभावित उत्तर देने के लिए बाध्य करता है, जो परीक्षण और मूल्यांकन के लिए आवश्यक होता है; or इसे चुपचाप 0.7 में बदल देता है, और आपको केवल ऐसे उत्तर मिलते हैं जो हर रन में अलग होते हैं।
डिफ़ॉल्ट्स के लिए हमेशा.get(key, default)का उपयोग करें — यह केवल तभी डिफ़ॉल्ट मान लेता है जब वह key वास्तव में गायब हो।orकेवल तभी सही होता है जब आप जानबूझकर शून्य और खाली मानों को भी छोड़ना चाहते हों।
YAML: इंसानों के लिए आसान, लेकिन यह डेटा टाइप्स का अनुमान लगाता है
कई टीमें YAML में कॉन्फ़िग लिखती हैं, क्योंकि यह टिप्पणियों (comments) की अनुमति देता है और इसमें ब्रैकेट्स व कोट्स कम होते हैं। पायथन में पहले से कोई YAML रीडर नहीं आता; प्रोजेक्ट में एक जोड़ें:
uv add pyyamlपैकेज का नाम pyyaml है, लेकिन इसे import yaml के रूप में इम्पोर्ट किया जाता है। अब एक कॉन्फ़िग पढ़ें:
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 strJSON के विपरीत, YAML आपसे प्रकार स्पष्ट रूप से लिखने की माँग नहीं करता — पार्सर ख़ुद अनुमान लगाता है। अधिकांश समय यह अनुमान सही होता है, लेकिन दो जगहों पर यह चुपचाप ग़लत हो जाता है। yes, no, on और off बूलियन बन जाते हैं — फ़ाइल में cache: off लिखा था और आपको False मिला। और 3.10 को एक दशमलव संख्या पढ़ लिया जाता है जिसका मान 3.1 है; वर्ज़न नंबर के रूप में यह पूरी तरह से एक अलग चीज़ है। कोट्स में लिखा "3.10" ही स्ट्रिंग बना रहता है।
सीधा नियम यह है: कोई भी मान जो संख्या या yes/no जैसा दिखता है लेकिन वास्तव में टेक्स्ट है — वर्ज़न नंबर, IDs, पिन कोड — उसे YAML में हमेशा कोट्स में रखें।
और हमेशा yaml.safe_load का उपयोग करें। पुराने ट्यूटोरियल्स में दिखने वाला yaml.load(text) अब TypeError: load() missing 1 required positional argument: 'Loader' के साथ पूरी तरह विफल हो जाता है। यह बदलाव जानबूझकर किया गया है: कुछ लोडर्स YAML फ़ाइल के अंदर दिए गए निर्देशों से मनमाने पायथन ऑब्जेक्ट्स बना सकते हैं, इसलिए किसी अविश्वसनीय YAML फ़ाइल को पढ़ने का मतलब किसी और का कोड चलाना हो जाता है। safe_load केवल बुनियादी प्रकार ही बनाता है — डिक्शनरीज़, सूचियाँ, स्ट्रिंग्स, संख्याएँ, बूलियन, None।
एनवायरनमेंट वेरिएबल्स: सब कुछ एक स्ट्रिंग है
API keys, डेटाबेस पते, कौन सा मॉडल इस्तेमाल करना है — वे मान जो मशीन-दर-मशीन बदलते हैं या जिन्हें सीक्रेट रखना ज़रूरी है, वे कोड या कमिट किए गए कॉन्फ़िग में नहीं, बल्कि एनवायरनमेंट वेरिएबल्स में रहते हैं। पायथन उन्हें os.environ या os.getenv से पढ़ता है। इस उदाहरण के लिए, दो वेरिएबल्स कोड के अंदर ही सेट किए गए हैं; वास्तविक काम में वे शेल, सर्वर या कंटेनर से आते हैं:
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
30एनवायरनमेंट वेरिएबल्स हमेशा स्ट्रिंग्स होते हैं। "false" एक गैर-रिक्त पाँच-वर्णों वाली स्ट्रिंग है, इसलिए if debug: इसे सत्य (True) मानता है — ठीक इसी तरह अध्याय की शुरुआत में सर्वर "false" सेट होने पर भी डिबग मोड में चल रहा था। इसी तरह, "8000" + "1" जोड़ नहीं बल्कि स्ट्रिंग संयोजन (concatenation) है। कोई एरर नहीं आता, बस प्रोग्राम ग़लत काम करता है।
इसलिए जैसे ही आप किसी एनवायरनमेंट वेरिएबल को पढ़ें, उसे तुरंत सही प्रकार में बदलें। संख्याओं के लिए, int() या float() पर्याप्त है — वे अमान्य इनपुट पर खुद ValueError देते हैं। बूलियन्स के लिए पायथन में कोई इन-बिल्ट कन्वर्ज़न नहीं है (bool("false") हमेशा True होता है), इसलिए आप अपना फ़ंक्शन लिखते हैं:
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'अंतिम पंक्ति ही इस फ़ंक्शन का असली मूल्य है। ग़लत स्पेलिंग वाला "flase" चुपचाप True या False नहीं बनता — प्रोग्राम शुरुआत में ही रुक जाता है और बताता है कि वह किस मान को नहीं समझ सका।
साथ ही os.environ["NAME"] और os.getenv("NAME") के बीच के अंतर को भी ध्यान में रखें। पहला वेरिएबल गायब होने पर KeyError: 'LLM_API_KEY' देता है; दूसरा चुपचाप None लौटाता है, जो कुछ मिनट बाद किसी API कॉल से 401 Unauthorized के रूप में सामने आ सकता है — जिस समय तक मूल कारण खोजना बहुत कठिन हो जाता है। जिस भी मान के बिना प्रोग्राम नहीं चल सकता, उसकी शुरुआत में ही जाँच करें।
.env फ़ाइल: आपकी अपनी मशीन के लिए एक एनवायरनमेंट
हर बार टर्मिनल खोलने पर दस वेरिएबल्स सेट करना व्यावहारिक नहीं है। इसका सामान्य समाधान प्रोजेक्ट रूट में एक .env फ़ाइल और python-dotenv पैकेज है, जो उस फ़ाइल को पढ़ता है और मानों को os.environ में डाल देता है:
uv add python-dotenvयहाँ भी पैकेज का नाम और इम्पोर्ट का नाम अलग है: पैकेज python-dotenv है, इम्पोर्ट dotenv है। एक .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 एक काल्पनिक मान है; कभी भी किसी दस्तावेज़, स्क्रीनशॉट या कमिट में वास्तविक key न रखें।) इसे पढ़ने का कोड, एक बार फिर स्क्रिप्ट के स्थान से पाथ बनाते हुए:
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...अब वही प्रोग्राम, लेकिन शेल (macOS/Linux या Git Bash) में पहले से LLM_MODEL सेट होने के साथ:
LLM_MODEL=chat-large uv run main.pyTrue
chat-large
true
sk-test...load_dotenv पहले से सेट वेरिएबल्स को नहीं बदलता। यह जानबूझकर और महत्वपूर्ण रूप से किया गया है: .env केवल आपकी अपनी मशीन की सुविधा के लिए है, और सर्वर या कंटेनर में सेट किया गया वास्तविक एनवायरनमेंट हमेशा सर्वोच्च प्राथमिकता रखता है। (load_dotenv(path, override=True) इस प्राथमिकता क्रम को उलट देता है, लेकिन ऐसा चाहने का शायद ही कोई कारण होता है।)
यह भी ध्यान दें कि फ़ाइल गायब होने पर load_dotenv कोई एरर नहीं देता — यह केवल False लौटाता है। इसलिए कभी यह मानकर न चलें कि ".env पढ़ लिया गया था"; आपको जिन मानों की आवश्यकता है उनकी अलग से जाँच करें, जैसा कि अगला उदाहरण करता है।
दो नियम जिनका पालन हर पेशेवर रिपॉजिटरी करती है:
.envको कभी कमिट नहीं किया जाता। यह.gitignoreमें जाता है। एक बार key Git इतिहास में दर्ज हो गई तो फ़ाइल डिलीट करने के बाद भी वह वहीं रहती है; एकमात्र सुरक्षित समाधान key को रद्द करना और नई जारी करना है।.env.exampleको कमिट किया जाता है। इसमें खाली या हानिरहित मानों के साथ वही वेरिएबल नाम होते हैं। नया सहकर्मी इसे.envमें कॉपी करता है और ठीक-ठीक जानता है कि किन मानों की आवश्यकता है।
एक संपूर्ण उदाहरण
अब सब कुछ एक साथ: सपोर्ट बॉट के लिए एक कॉन्फ़िग लोडर। प्रोजेक्ट को uv init --app --python 3.12 support-bot, फिर uv add pyyaml python-dotenv के साथ बनाया गया था। फ़ोल्डर संरचना:
support-bot/
├── .env
├── .env.example
├── config/
│ └── app.yaml
├── prompts/
│ ├── summarise.txt
│ └── system.txt
├── pyproject.toml
├── settings.py
└── uv.lockconfig/app.yaml — कमिट किया गया, इसमें कोई सीक्रेट नहीं है:
app:
name: support-bot
debug: false
llm:
model: chat-small
temperature: 0.2
max_tokens: 512
retrieval:
top_k: 4.env — कमिट नहीं किया गया:
# Local values for this machine. Never commit this file.
LLM_API_KEY=sk-test-placeholder-1234
APP_DEBUG=true.env.example — कमिट किया गया:
# Copy to .env and fill in. This file is committed; .env is not.
LLM_API_KEY=
APP_DEBUG=falseprompts/system.txt में एक पंक्ति है, You are a support assistant. Answer only from the provided context., और prompts/summarise.txt में है Summarise the ticket below in two sentences.
और 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'])}")चार अलग-अलग रन — सामान्य, शेल से दो मान बदले जाने पर, .env हटाए जाने पर, और एक अमान्य टेम्परेचर के साथ:
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(तीसरे रन से पहले .env फ़ाइल का नाम बदलकर अलग कर दिया गया था, और चौथे से पहले उसे वापस रख दिया गया था।) अंतिम दो रन एग्ज़िट कोड 1 के साथ समाप्त होते हैं और पहले दो 0 के साथ, जिससे कोई डिप्लॉय स्क्रिप्ट या CI आसानी से जान सकती है कि कॉन्फ़िग ग़लत है।
इस लोडर के पीछे के तकनीकी निर्णय:
प्रत्येक पाथ PROJECT_ROOT से आता है। Path(__file__).resolve().parent की गणना एक बार की जाती है और अन्य सभी पाथ्स उसी पर बनाए जाते हैं। प्रोग्राम जिस भी फ़ोल्डर से शुरू हो, वह हमेशा उन्हीं फ़ाइलों को पढ़ता है।
प्राथमिकता का एक स्पष्ट क्रम (Explicit precedence)। पहले YAML डिफ़ॉल्ट्स, उनके ऊपर .env, और शेल या सर्वर का वास्तविक एनवायरनमेंट सबके ऊपर। दूसरे रन में एक भी फ़ाइल को एडिट किए बिना मॉडल और टेम्परेचर बदल गए — प्रोडक्शन में कॉन्फ़िग को एक ही जगह से इसी तरह नियंत्रित किया जाता है।
temperature 0.0 सुरक्षित रहता है। os.getenv("LLM_TEMPERATURE", ...) केवल तभी YAML मान पर वापस जाता है जब वेरिएबल वास्तव में अनुपस्थित हो — यहाँ कहीं भी or नहीं है। इसी तरह debug यह जांचता है कि "APP_DEBUG" in os.environ, न कि यह कि मान ट्रुथी (truthy) है या नहीं।
टाइप कन्वर्ज़न और रेंज चेकिंग एक ही जगह। float(), int(), parse_bool() और टेम्परेचर की 0 <= temperature <= 2 जाँच सब load_settings के अंदर रहती है। बाकी प्रोग्राम सुरक्षित रूप से मान सकता है कि settings["temperature"] एक मान्य संख्या है।
एक समर्पित ConfigError। प्रत्येक कॉन्फ़िग समस्या एक ही प्रकार के एरर के रूप में सामने आती है, और __main__ ब्लॉक इसे एक स्पष्ट एक-पंक्ति संदेश में बदल देता है — ट्रेसबैक के बजाय यह बताता है कि कौन सा मान ठीक करना है।
सीक्रेट्स कभी भी पूरे प्रिंट नहीं होते। mask पहले तीन कैरेक्टर्स दिखाता है, जिससे आप जान सकते हैं कि किस तरह की key लोड हुई थी, लेकिन key कभी लॉग्स तक नहीं पहुँचती। मास्क की लंबाई भी तय (fixed) है, इसलिए key की वास्तविक लंबाई से कुछ भी पता नहीं लगाया जा सकता।
glob के ज़रिए प्रॉम्प्ट फ़ाइलें। prompts/ में एक नई .txt फ़ाइल डालें और वह कोड को छुए बिना settings["prompts"] में दिखाई देने लगती है। sorted() इसलिए है क्योंकि glob जिस क्रम में फ़ाइलें लौटाता है वह ऑपरेटिंग सिस्टम पर निर्भर करता है।
एक सीमा को जान लेना ज़रूरी है: load_settings एक सादा डिक्शनरी लौटाता है, इसलिए settings["tempreature"] जैसा टाइपो केवल रनटाइम पर KeyError के रूप में पकड़ा जाता है, और प्रत्येक नई सेटिंग के लिए कन्वर्ज़न और चेकिंग हाथ से लिखनी पड़ती है। इसे घोषणात्मक रूप से करने वाला टूल कोर्स में आगे आएगा; यहाँ के सिद्धांत — एक ही स्थान पर लोड करना, सही टाइप्स, शुरुआत में ही रुकना — उसके साथ भी बिल्कुल वही रहेंगे।
जब यह काम न करे
FileNotFoundError: [Errno 2] No such file or directory: 'config\\app.json' पाथ रिलेटिव है और प्रोग्राम किसी अन्य फ़ोल्डर से शुरू किया गया था। पाथ को Path(__file__).resolve().parent से बनाएं। यह देखने के लिए कि लुकअप किस फ़ोल्डर से शुरू हो रहा है, Path.cwd() प्रिंट करें।
TypeError: unsupported operand type(s) for /: 'str' and 'str' "config" / "app.json" — दोनों तरफ स्ट्रिंग्स हैं। सबसे बाएँ हिस्से को Path बनाएं: Path("config") / "app.json"।
json.decoder.JSONDecodeError: Expecting property name enclosed in double quotes: line 1 column 36 (char 35) JSON में अंतिम आइटम के बाद एक अतिरिक्त कॉमा (trailing comma) है ({"top_k": 4,}), या सिंगल कोट्स में कोई key है। पायथन डिक्शनरीज़ इन्हें स्वीकार कर लेती हैं; JSON नहीं करता। संदेश में कॉलम नंबर ठीक वहीं दिखाता है जहाँ पार्सर अटक गया था।
TypeError: load() missing 1 required positional argument: 'Loader' पुराना कोड जो yaml.load(text) को कॉल कर रहा है। इसके बजाय yaml.safe_load(text) लिखें।
KeyError: 'LLM_API_KEY' कोड ने os.environ["LLM_API_KEY"] पढ़ा और वेरिएबल सेट नहीं है। जांचें कि .env प्रोजेक्ट रूट में है, load_dotenv को सही पाथ मिला है, और नाम हर जगह एक समान लिखा गया है। गायब मानों के लिए स्पष्ट संदेश देने के लिए, ऊपर दिए गए require_env जैसे फ़ंक्शन का उपयोग करें।
ModuleNotFoundError: No module named 'dotenv' python-dotenv को प्रोजेक्ट में कभी जोड़ा नहीं गया था, या प्रोग्राम uv run के बिना चलाया गया था, इसलिए प्रोजेक्ट का एनवायरनमेंट इस्तेमाल नहीं हुआ। uv add python-dotenv चलाएं, और प्रोग्राम को uv run से शुरू करें।
चरण 4 / 6 — अनुमान
अपनी समझ की जाँच करें
The server has APP_DEBUG=false set (shown here by setting it inside the code). What prints?
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'`
For an evaluation run, the config sets temperature to 0 so the model answers the same way every time. But the answers differ on every run, and there is no error. Where is the 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, so `or` discards it and takes `0.7`; it should be `config["llm"].get("temperature", 0.7)`
- B`.get("temperature")` does not work on nested dictionaries; it must be `config["temperature"]`
- CUsing single quotes inside the f-string changes the value of `temperature`
- DThe `0` coming from JSON is really the string `"0"`, so the comparison fails
This code lives in scripts/show_config.py, and the config is at config/app.json in the project root. It works from the root and gives FileNotFoundError from scripts/. What is the best fix?
from pathlib import Path
config_path = Path("config/app.json")
print(config_path.read_text(encoding="utf-8"))- AWrite the path as `"../config/app.json"`
- BKeep a copy of `config/app.json` inside the `scripts/` folder as well
- C`Path(__file__).resolve().parent.parent / "config" / "app.json"` — build the path from the script's own location
- DTell everyone the script must only be run from the project root
प्ले प्रो का हिस्सा है
+30 XP इंतज़ार में
प्रो के साथ यह अध्याय खेलें
पढ़ना मुफ़्त है, और पूरा अध्याय खुला है। हर मुफ़्त कोर्स के पहले 5 अध्याय मुफ़्त में खेले जा सकते हैं। अध्याय 6 से प्रश्नों के उत्तर देना, XP कमाना और स्ट्रीक बढ़ाना प्रो का हिस्सा है।
प्रो से प्ले अनलॉक करेंआपकी बारी
एक ऐसा कॉन्फ़िग लोडर लिखें जो बेस कॉन्फ़िग के ऊपर एनवायरनमेंट-विशिष्ट कॉन्फ़िग को परत (layer) के रूप में चढ़ाता है। uv init --app --python 3.12 config-lab से प्रोजेक्ट बनाएं, uv add python-dotenv चलाएं, फिर ये फ़ाइलें बनाएं:
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: एक पंक्ति,APP_ENV=staging
main.py इस प्रकार लिखें कि:
- प्रत्येक पाथ
Path(__file__).resolve().parentसे बनाया जाए, ताकि प्रोग्राम किसी भी फ़ोल्डर से एक ही परिणाम दे APP_ENVएनवायरनमेंट या.envसे आए, जिसका डिफ़ॉल्ट मानdevहो; ऐसी एनवायरनमेंट जिसके लिए कोई फ़ाइल मौजूद न हो (जैसेconfig/prod.json), प्रोग्रामconfig error: no config/prod.jsonप्रिंट करे और एग्ज़िट कोड1के साथ रुक जाए- दोनों कॉन्फ़िग्स का गहरा विलय (deep merge) हो: नेस्टेड डिक्शनरीज़ पूरी तरह बदले जाने के बजाय key-by-key मर्ज हों, ताकि
base.jsonकाmodelबचा रहे भले हीstaging.jsonमें कोई मॉडल न हो 0काtemperatureशून्य (0) ही रहे — कोईorनहीं
प्रोजेक्ट रूट से uv run main.py चलाने पर ठीक यह आउटपुट मिलना चाहिए:
env staging
model chat-small
temperature 0
top_k 8फिर दो चीज़ों का परीक्षण करें। पहला, APP_ENV=prod uv run main.py चलाएं और जांचें कि प्रोग्राम स्पष्ट संदेश के साथ रुकता है। दूसरा, गहरे विलय के बजाय साधारण {**base, **overlay} आज़माएँ और config["llm"] प्रिंट करें — model कहाँ गया, और मर्ज लाइन ने खुद कोई एरर क्यों नहीं दिया?
चरण 6 / 6
चुनौती — अध्याय की क्विज़
सरल से कठिन — दस प्रश्न, अंतिम वाले जानबूझकर चुनौतीपूर्ण बनाए गए हैं।
प्रो के साथ खेलें