runtime
197 / 250
lesson 197 Software Architecture · advanced

Configuration: defaults, files and environment

Page sizes, currencies and connection strings differ between your laptop, the test server and production. Configuration keeps them out of the code: defaults, overridden by a file, overridden by environment variables, checked once.

You'll be able to
  • Merge defaults, a settings file and environment overrides in a fixed precedence
  • Convert environment text to the right types, ignoring unrelated variables
  • Validate once into a frozen dataclass that the rest of the program receives
The idea

Hard-coded values (PAGE_SIZE = 50 buried in a function) force a code change for every environment. Instead, collect settings in layers, each overriding the one before: built-in defaults, then a settings file (JSON or TOML) for this deployment, then environment variables for the machine or container (often secrets and per-server values). Environment variables are always text, so they must be converted to the type each setting expects. Read the environment as a dictionary passed in (os.environ in production), so tests can pass a plain dictionary (the CLI & Automation module, the Advanced OOP & Design module).

Run each example to see what it prints. Running examples doesn't count toward progress.

example-1.py · Three layers, one result
import json

DEFAULTS = {"page_size": 50, "currency": "USD", "debug": False}

def from_env(environ, prefix, defaults):
    overrides = {}
    for key, text in environ.items():
        if not key.startswith(prefix):
            continue
        name = key[len(prefix):].lower()
        if name not in defaults:
            continue
        default = defaults[name]
        if isinstance(default, bool):
            overrides[name] = text.lower() in ("1", "true", "yes")
        elif isinstance(default, int):
            overrides[name] = int(text)
        else:
            overrides[name] = text
    return overrides

file_settings = json.loads('{"page_size": 100, "currency": "GBP"}')
environ = {"SHOP_CURRENCY": "EUR", "SHOP_DEBUG": "true", "PATH": "/usr/bin", "SHOP_THEME": "dark"}
settings = {**DEFAULTS, **file_settings, **from_env(environ, "SHOP_", DEFAULTS)}
print(settings)

Validate the merged settings once, at start-up, and hand the rest of the program an object, not a loose dictionary. A dataclass with frozen=True (the Data Models & Typing module introduced dataclasses) can't be changed after it is created, so no part of the program can quietly modify a setting another part relies on, and __post_init__ is the natural place to reject bad values with a clear message.

example-2.py · A validated, frozen settings object
from dataclasses import dataclass, FrozenInstanceError

@dataclass(frozen=True)
class Settings:
    page_size: int
    currency: str
    debug: bool

    def __post_init__(self):
        if not 1 <= self.page_size <= 500:
            raise ValueError(f"page_size must be 1-500, got {self.page_size}")
        if self.currency not in ("USD", "EUR", "GBP"):
            raise ValueError(f"unsupported currency {self.currency!r}")

settings = Settings(**{"page_size": 100, "currency": "EUR", "debug": True})
print(settings)
try:
    settings.page_size = 5
except FrozenInstanceError as error:
    print(type(error).__name__)
try:
    Settings(page_size=0, currency="USD", debug=False)
except ValueError as error:
    print(error)

When to use it: for anything that differs between environments or that operators should change without a code release: limits, URLs, feature switches, credentials (credentials only from the environment or a secrets store, never from the repository). Mistakes to avoid: reading os.environ all over the program (read once, pass the settings object); forgetting that environment values are strings ("false" is truthy!); silently ignoring invalid values; and mutating the defaults dictionary while merging.

Review
  • Layers: defaults < settings file < environment; later layers win.
  • Environment values are text: convert by type, and ignore unrelated variables.
  • Validate once into a frozen object and pass it to the code that needs it.
  • Next: keeping the core independent of storage and services.
Practise this

Loading your progress…

To master this lesson, solve both challenges without hints or the answer, and pass the concept check.

Configuration: defaults, files and environment — Software Architecture · Runtime