Configuration bugs often hide inside one question: does an empty value mean “unset,” “zero,” or “invalid”?
Build a resolver with four layers:
default < config file < environment < command line
Do not use truthiness to choose a value. 0, false, and "" are different inputs with different validation rules.
from dataclasses import dataclass
@dataclass
class Resolved:
value: int
source: str
def parse_port(raw, source):
if raw is None:
return None
if raw == "":
raise ValueError(f"empty port from {source}")
value = int(raw)
if not 0 <= value <= 65535:
raise ValueError(f"invalid port from {source}")
return Resolved(value, source)
Resolve from highest to lowest precedence, stopping at the first value that is present, then validate it. Do not silently fall through when a high-priority source is malformed; that hides deployment mistakes.
Table-driven tests make the contract visible:
cases = [
({}, 8080, "default"),
({"PORT": "0"}, 0, "environment"),
({"PORT": ""}, ValueError, "environment"),
({"PORT": "abc"}, ValueError, "environment"),
]
Add --explain-config so users can inspect the winning source without exposing secrets:
port=8080 source=config-file
host=127.0.0.1 source=default
api_key=<redacted> source=environment
Python’s argparse documentation covers command-line defaults and parsing. The learning goal here is to keep parsing, precedence, validation, and explanation separate so each can be tested.
Extension exercise: add a --no-color boolean and distinguish an omitted flag from an explicit false value. Then serialize the resolved, non-secret settings in a startup test and compare them with an approved snapshot. That small change catches many production configuration mistakes before a framework obscures them.
Top comments (0)