A video-metadata extractor loads settings with tomllib and lets os.environ override them per deployment — how do you build that layering so a bad override fails at startup?
answer
- one resolution step, not scattered lookups
- layers with a written-down precedence
- every environment value arrives as text
- booleans need a vocabulary, not truthiness
- validate before the first item is processed
basics
~20 sResolve the layers once at startup: parse the TOML with tomllib, look up a prefixed os.environ name per key, coerce that string to the type the file declared, then validate the merged result and abort before any work begins.
solid answer
~50 sMake configuration resolution a single startup step with an explicit precedence — built-in defaults, then the TOML file, then `os.environ`, then command-line flags — producing one frozen settings object that the rest of the process only reads. The hard part is types: `tomllib` gives you real `int`, `bool` and `str` values, but every `os.environ` value is a `str`, so each override needs an explicit conversion, with a real word-to-boolean mapping because `bool("false")` is `True`. Test presence with `"NAME" in os.environ`, not truthiness, so an intentionally empty override is distinguishable from an unset one. Validate everything — ranges, paths, mutually exclusive options — while the process is still doing nothing, so a typo cannot surface after the extractor has already written half a batch of outputs. Finally, log the effective configuration and where each value came from; that log is what makes an incident diagnosable.
code
python · 28 linesimport io
import os
import tomllib
TRUE = {"1", "true", "yes", "on"}
FALSE = {"0", "false", "no", "off"}
def coerce(text, template):
if isinstance(template, bool): # bool before int: bool subclasses int
lowered = text.strip().lower()
if lowered in TRUE:
return True
if lowered in FALSE:
return False
raise ValueError(f"not a boolean: {text!r}")
return type(template)(text)
document = b"[extract]\nworkers = 4\nprobe_streams = true\n"
file_settings = tomllib.load(io.BytesIO(document))["extract"]
os.environ["VIDMETA_WORKERS"] = "12"
os.environ["VIDMETA_PROBE_STREAMS"] = "off"
resolved = {}
for key, value in file_settings.items():
name = "VIDMETA_" + key.upper()
resolved[key] = coerce(os.environ[name], value) if name in os.environ else value
print(resolved)go deeper
Know that a TOML parser returns typed values while every environment variable is a string, and that overriding one with the other therefore needs an explicit conversion. Reading settings once at startup is the habit to form.
Explain the mechanics: a documented precedence order, presence tests rather than truthiness, an explicit boolean word list, and why isinstance must check bool before int when coercing to an existing value's type.
Demonstrate operational judgement: validate the merged configuration before any work starts so errors never land inside a partial-failure rollback, refuse present-but-invalid values instead of defaulting, and log the effective settings with each value's origin.
Own the contract between the code and whoever operates it. Decide what belongs in a baked file versus a per-deployment variable when releases are weeks apart, how unknown or mistyped overrides are handled fleet-wide, and how configuration changes get reviewed.
## The shape of the problem A video-metadata extractor runs the same image in several deployments. The stable settings — worker count, probe timeout, which streams to inspect, output directory — live in a TOML file baked into the image. Per-deployment differences arrive as environment variables, because that is what an orchestrator can set without a rebuild. On a three-week release train, that environment layer is the only knob you can turn between trains, which is precisely why it deserves the same rigour as code. ## Precedence, decided once and written down Pick an order and document it: built-in defaults, then the TOML file, then `os.environ`, then command-line flags, each layer overriding the one before. Resolve it **once**, at startup, into a single settings object — a frozen dataclass or a read-only mapping — and pass that object around. The alternative, calling `os.environ.get()` deep inside the code that needs a value, produces a program whose behaviour depends on when a lookup happened and which cannot report its own configuration. `collections.ChainMap` is a reasonable tool for the lookup itself, but be clear about its limit: it chains mappings at the top level only. A TOML file with nested tables (`[extract]`, `[output]`) is a nested dict, so you chain *per table*, or flatten first. A ChainMap over the whole document will silently let one layer's whole table hide another's. ## The type boundary is the interesting part `tomllib` returns typed values: `workers = 4` is an `int`, `probe_streams = true` is a `bool`. Environment variables are always `str`. So every override crosses a type boundary and every crossing needs an explicit conversion: * **Booleans need a vocabulary, not truthiness.** `bool("false")` is `True`, and so is `bool("0")`. Map a fixed set of words — `1/true/yes/on` and `0/false/no/off` — and raise on anything else. This is exactly what `configparser`'s `getboolean` does, and the reason it exists. * **`bool` is a subclass of `int`.** A generic "coerce to the type of the existing value" helper must test `isinstance(value, bool)` *before* `isinstance(value, int)`, or the boolean path is never taken and `"0"` becomes `True` again. * **Presence, not truthiness.** `if os.environ.get("VIDMETA_PREFIX"):` treats a deliberate empty string as absent. Use `"VIDMETA_PREFIX" in os.environ` and decide separately whether empty is legal for that key. * **Unknown keys.** Decide whether `VIDMETA_WORKRES=8` is ignored or fatal. Ignoring typos is how an operator spends an afternoon wondering why the override did nothing; rejecting an unrecognised prefixed variable is usually the kinder policy. ## Fail at startup, because of the rollback boundary The extractor's failure mode is a partial-failure rollback: a batch that dies halfway has already written some metadata records and not others, and someone has to undo them. Every configuration error you can detect before the first item is processed is one that never enters that rollback path. So the resolution step ends with validation — numeric ranges, that the output directory exists and is writable, that mutually exclusive options are not both set — and raises on the first problem while the process has done nothing. A configuration error discovered at item 4,000 of 10,000 is a config bug that has become an operations incident. The corollary: a bad value must not be silently replaced by a default. Defaults belong to *missing* values. An option that is present and unparsable is an operator mistake, and the correct response is a non-zero exit with the variable name and the offending value in the message. ## Practicalities * `os.environ` is a live view of the process environment and is mutable; snapshot it at resolution time so nothing later re-reads a changed value. Child processes inherit it, so a variable you set for yourself is also visible to anything you spawn. * Prefix your variables (`VIDMETA_`) so the namespace is greppable, and derive the environment name from the config key mechanically rather than maintaining a hand-written table that drifts. * Log the effective configuration at startup with the origin of each value — file, environment, or default — omitting anything you would not want in a log. That single line answers most "why did this run behave differently?" questions. * `tomllib` cannot write, so you cannot round-trip the resolved configuration back to the file with the standard library. Config is input, not state. * If the file is INI rather than TOML, `configparser.ConfigParser(defaults=os.environ)` puts the whole environment into the `DEFAULT` layer so values can interpolate `%(home)s`; keys pass through `optionxform`, which lower-cases them. It is neat, but it also makes every section inherit hundreds of options, so an explicit per-key override loop is usually clearer.
- Why not just call os.environ.get() at the point of use instead of resolving once?Because the program then has no single answer to "what is my configuration?". Values can differ between two lookups, nothing validates them before work starts, tests must mutate global state to exercise a branch, and you cannot log the effective settings. One resolution step plus a frozen object fixes all four.
- collections.ChainMap looks made for this. Where does it fall short?It chains mappings at the top level only. A TOML document with nested tables is a dict of dicts, so a ChainMap over whole documents lets one layer's table hide another's entirely rather than merging key by key. Chain per table, or flatten the keys before chaining.
- An operator sets VIDMETA_WORKRES=8 by mistake. What should the process do?Ideally refuse to start. If you own the whole prefixed namespace you can enumerate the variables carrying it and reject any that map to no known setting, naming the typo. Silently ignoring it produces the worst outcome: an override that appears applied and is not.
- Should a present-but-invalid value fall back to the default?No. Fallbacks belong to missing values. A value that is present and unparsable is an operator mistake, and swallowing it hides the mistake until the wrong behaviour shows up in output. Exit non-zero with the variable name and the offending value in the message.
saying these in an interview costs you the question
- Calls bool() on an environment string for a flag
- Uses truthiness to test whether a variable is set
- Reads os.environ deep inside business logic
- Silently falls back to a default on an invalid value
- Assumes ChainMap merges nested tables key by key
- Discovers configuration errors only when work has begun