Multi-Environment Settings Override

The schema must be identical in dev, staging, and production; only the values differ. The failure mode is a per-environment settings subclass that quietly grows different fields until “works in staging” stops meaning anything. This page layers environment-specific values over one schema. It rounds out type-safe validation with pydantic-settings alongside schema evolution.

Secure implementation

# config/settings.py
import os
from pydantic import SecretStr
from pydantic_settings import BaseSettings, SettingsConfigDict

ENV = os.environ.get("APP_ENV", "dev")        # dev | staging | production

class Settings(BaseSettings):
    # One schema; the env_file chooses which values layer in.
    model_config = SettingsConfigDict(
        env_file=(".env", f".env.{ENV}"),     # later file wins; real env wins over both
        extra="forbid",
    )
    database_url: str
    api_key: SecretStr
    debug: bool = False                       # safe default, flipped per environment
    feature_new_checkout: bool = False

settings = Settings()

A single class is used everywhere. env_file layers a base .env and an environment overlay; real OS environment variables (injected in staging/production) still outrank both, preserving precedence. There are no diverging subclasses.

Configuration reference

Element Type Default Security implication
APP_ENV str "dev" Selects the overlay file only
env_file tuple files Later file wins; env wins over files
extra="forbid" bool Keeps every environment on one schema
feature flag field bool False Off by default; flip per environment
SecretStr masked Secrets never logged across environments

Step-by-step deployment parity

  1. Local devAPP_ENV=dev; values come from .env + .env.dev.
  2. CIAPP_ENV=staging with injected variables overriding the overlay.
  3. Staging/Production — the orchestrator sets APP_ENV and injects real variables that outrank the overlay file; the same Settings class validates them.

Security boundaries & operational guardrails

  • One settings class, identical fields, for every environment — no subclasses.
  • Real environment variables always outrank .env* files (override=False precedence).
  • Feature flags default to off and are flipped per environment, never hard-coded on.
  • Overlay files for non-prod are uncommitted or carry no real secrets.
  • extra="forbid" guarantees an environment cannot quietly add an unvalidated key.

Troubleshooting

  • Staging behaves unlike production — a value differs in the overlay or an injected variable; log the resolved values (not secrets) per environment.
  • Feature flag stuck off — the boolean string was not parsed; route it through the model, not bool(). See Feature Flags with Pydantic Settings.
  • Overlay overrides an injected secret — files must sit below env vars; check the source order.
  • extra not permitted — one environment sets a key the schema lacks; add it to the single schema or stop setting it.

Frequently asked questions

How do I override pydantic-settings per environment?

Keep one schema and vary only the values. Select an environment-specific .env file (.env.staging, .env.production) via env_file, and let real OS environment variables override it. Never create a separate settings subclass per environment.

Should I have one settings class per environment?

No. Diverging subclasses drift apart and defeat parity. Use a single class whose fields are identical everywhere; the environment supplies different values through env files and injected variables.

How do feature flags fit into multi-environment settings?

Model them as boolean fields on the same settings object with safe defaults, then flip them per environment via variables. They validate and document themselves like any other setting.

Conclusion

The invariant: one schema, many value layers, real environment variables always on top. Parity is preserved because every environment validates against the identical model.