12-factor config precedence in Python

The 12-factor app says “store config in the environment,” but a real service also has .env files, config files, and defaults. Reconciling them with the 12-factor ideal means putting the environment on top of a deterministic ladder. This page does that, extending Configuration Precedence Rules.

Problem 1: config baked into code

# ANTI-PATTERN: config that differs per environment lives in code
if socket.gethostname().startswith("prod"):    # config branching on hostname
    DATABASE_URL = "postgres://prod-db/app"

This violates factor III — config that varies between deploys must live in the environment, not in if branches.

Problem 2: the environment not actually winning

# ANTI-PATTERN: file config overrides the environment
DATABASE_URL = file_config.get("database_url") or os.environ["DATABASE_URL"]

Here the checked-in file outranks the environment — the opposite of the 12-factor rule.

Secure implementation

# config/twelve_factor.py
import os
from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    # Environment is authoritative; .env only seeds local gaps; defaults are last.
    model_config = SettingsConfigDict(env_file=".env", extra="forbid")
    database_url: str                       # required from the environment
    log_level: str = "INFO"                 # default is the lowest-priority fallback

settings = Settings()
# pydantic-settings already ranks: init > OS env > .env > defaults — exactly 12-factor.

pydantic-settings encodes the 12-factor order natively: OS environment variables outrank the .env file, which outranks defaults. No hostname branching, no file overriding the environment.

Gotchas & version-specific behaviour

  • Factor III wants config that varies between deploys in the environment; truly constant values can stay as defaults.
  • Secrets are config too — keep them in the environment or a secret store, never in code.
  • One codebase, many deploys (factor I/X): the same image reads different environment values.
  • extra="forbid" enforces that every environment supplies exactly the expected keys.

Production parity checklist

  • No config branches on hostname, ENV strings, or build flags.
  • OS environment variables outrank .env and defaults.
  • The same image runs in every environment with different injected values.
  • Secrets come from the environment or a managed store.
  • Required keys validated at startup.

Conclusion

The 12-factor ideal and a precedence ladder agree: the environment wins, files seed gaps, defaults are last, and nothing varies in code. For the full source-order model, see Configuration Precedence Rules.