Configuration Precedence Rules
When DATABASE_URL is set in the shell, in a .env file, and in config.yaml, exactly one of them must win — and it must be the same one on every developer’s laptop and in production. Undefined precedence is the root cause of the “works on my machine” class of bug. This page makes the order explicit and deterministic.
Precedence is the rule that ties the configuration sources together: it decides whether an environment variable or a .env file value reaches the application.
Secure implementation
# config/precedence.py
from typing import Any, Mapping
# Sources are ordered highest-priority first.
def resolve(key: str, *sources: Mapping[str, Any]) -> Any:
for source in sources:
value = source.get(key)
if value is not None:
return value # first non-None hit wins
raise KeyError(f"No source provided a value for {key!r}")
# cli > env > dotenv > file > defaults
DATABASE_URL = resolve(
"DATABASE_URL", cli_args, os.environ, dotenv_dict, file_config, defaults
)
The order is encoded in one place, as the argument order to resolve. There is no environment-specific branching, so the precedence is provably identical everywhere it runs.
Configuration reference
| Source | Priority | Set where | Notes |
|---|---|---|---|
| CLI flags | 1 (highest) | argparse / CLI |
Explicit operator override |
| OS environment | 2 | shell / orchestrator | Wins over .env for parity |
.env file |
3 | local file | override=False keeps it below env |
| Config file | 4 | repo / mounted | Structured, non-secret values |
| Defaults | 5 (lowest) | settings model | Safe fallbacks only |
Deployment parity: local to production
- Local dev — defaults plus a
.envsupply most values; the shell can override ad hoc. - CI — pipeline variables sit at the environment level; no
.envpresent. - Staging/Production — the orchestrator sets environment variables that outrank everything below; defaults catch only non-critical values.
Security boundaries & guardrails
- Define the order exactly once; never re-implement it per module or per environment.
- Keep
.envstrictly below OS environment (override=False) so injected secrets win. - Treat secrets as environment/secret-store sourced only — never as a checked-in config-file default.
- Log the resolved source of each key (not its value) at startup for auditability.
Troubleshooting
- A value differs between laptop and prod — a source is set in one environment but not the other; log which source supplied the key.
.envvalue ignored — the key exists inos.environ, which outranks.env. Intended behaviour.- CLI flag has no effect — it was added to the wrong (lower) position in the source order.
- Default leaks to production — a required key was unset in the target environment; add a CI check that every required key resolves above the defaults layer. See CLI vs Env vs File Precedence.
Frequently asked questions
What is the standard configuration precedence order in Python?
Highest to lowest: CLI flags, then OS environment variables, then the .env file, then a checked-in config file, then hard-coded defaults. The first source that supplies a value wins.
Why does pydantic-settings ignore my .env value?
pydantic-settings gives OS environment variables higher priority than the .env file by default. If the key is already set in the real environment, that value wins — which is correct for production parity.
How do I make precedence identical between local and production?
Express the order once in a single resolver or settings model and run that same code everywhere. Never branch the precedence logic on an ENV string; only the values should differ.
Conclusion
The invariant: one declared order, the first non-empty source wins, and that order is byte-for-byte identical in every environment. Encode it once and log the winning source so drift is visible.