Check deployment settings at startup
The same image runs in development, staging and production; only its settings differ, and they arrive as environment variables, which are always text. Convert and check every setting when the program starts, report all the problems at once, and hand the rest of the program a frozen, typed settings object.
- Explain why
bool("false")isTrueand convert flags, numbers and durations from text explicitly - Validate all settings, including rules that involve two of them, and report every problem in one error
- Freeze the settings in a dataclass so nothing changes them while the program runs
The Software Architecture module merged defaults, a file and environment overrides into one configuration. At deployment time the environment is usually the whole story: the platform sets APP_ENV=prod, PORT=8080, DATABASE_URL=… for each environment, and one image serves them all (the twelve-factor rule: *config lives in the environment*). Two facts make that tricky. Every environment variable is text. And a mistake in one (a typo, a missing value) should stop the deployment at startup, not crash an hour later on the first request that reads it.
So a deployable program loads its settings once, at startup: read each variable, convert it explicitly (never with bool(), which is True for any non-empty text, "false" included), check its range or allowed values, check the rules *between* settings (debug mode must be off in production), and collect every problem before raising, so one deploy attempt shows them all. The result goes into a frozen dataclass (see Data Models & Typing): typed fields, and an error if code later tries to change one.
Run each example to see what it prints. Running examples doesn't count toward progress.
import os os.environ["DEBUG"] = "false" os.environ["WORKERS"] = " 4 " print(bool(os.environ["DEBUG"]), int(os.environ["WORKERS"])) TRUE = {"1", "true", "yes", "on"} FALSE = {"0", "false", "no", "off"} def parse_bool(text): value = text.strip().lower() if value in TRUE: return True if value in FALSE: return False raise ValueError(f"not a true/false value: {text!r}") print(parse_bool(os.environ["DEBUG"]), parse_bool("Yes")) try: parse_bool("flase") except ValueError as err: print(err)
Once loaded, settings shouldn't change. A frozen dataclass enforces that: assigning to a field raises an error, so no part of the program can quietly switch production into debug mode.
from dataclasses import dataclass @dataclass(frozen=True) class Settings: app_env: str debug: bool settings = Settings(app_env="prod", debug=False) settings.debug = True
from dataclasses import dataclass @dataclass(frozen=True) class Settings: app_env: str port: int debug: bool database_url: str def parse_bool(text): value = text.strip().lower() if value in {"1", "true", "yes", "on"}: return True if value in {"0", "false", "no", "off"}: return False raise ValueError def load_settings(environ): errors = [] app_env = environ.get("APP_ENV", "") if app_env not in {"dev", "staging", "prod"}: errors.append(f"APP_ENV must be dev, staging or prod, not {app_env!r}") port = 8000 try: port = int(environ.get("PORT", "8000")) if not 1 <= port <= 65535: errors.append("PORT must be 1 to 65535") except ValueError: errors.append("PORT must be a whole number") debug = False try: debug = parse_bool(environ.get("DEBUG", "false")) except ValueError: errors.append("DEBUG must be true or false") if app_env == "prod" and debug: errors.append("DEBUG must be off in prod") database_url = environ.get("DATABASE_URL", "") if not database_url: errors.append("DATABASE_URL is required") if errors: raise ValueError("; ".join(errors)) return Settings(app_env, port, debug, database_url) print(load_settings({"APP_ENV": "staging", "PORT": "8080", "DATABASE_URL": "sqlite:///market.db"})) try: load_settings({"APP_ENV": "prod", "PORT": "80800", "DEBUG": "true"}) except ValueError as err: print(err)
When to use it: once, at the start of every deployable program; the result is passed to the code that needs it. Mistakes to avoid: bool() on flags; reading os.environ deep inside the code (settings are then checked only when that code runs); stopping at the first problem; defaults for things that must differ per environment (a database URL, a secret: the Secure Python module); and printing the settings object without masking secrets.
- Environment settings are text: convert them explicitly; flags use an allow-list, never
bool(). - Check every setting and the rules between them at startup, and report all problems in one error.
- Freeze the result in a dataclass and pass it to the code that needs it.
- Next: before switching traffic to a new version, check that it is actually ready.