Config precedence rules: CLI vs environment vs file

Silent configuration mismatches frequently disrupt containerized Python deployments. Orchestration platforms inject environment variables that unintentionally shadow explicit CLI arguments. Understanding Configuration Precedence Rules eliminates this ambiguity. Explicit hierarchy enforcement prevents staging credentials from leaking into production workloads.

The failure manifests when a custom loader evaluates sources sequentially without a unified hierarchy. A service receiving --db-host=prod-db connects to staging-db because DB_HOST was injected earlier. Standard libraries like argparse, os.environ, and python-dotenv lack deterministic ordering. This violates strict type safety and breaks environment parity across deployment stages.

Reproducing the Incident

  1. Export DB_HOST=staging-db in the host environment.
  2. Mount a configuration file containing db_host: legacy-db.
  3. Execute python app.py --db-host=prod-db.
  4. Observe the application routing to the environment or file value.
  5. Verify logs show incorrect connections without override warnings.

Root Cause Analysis

Root cause analysis reveals that naive os.environ.get() calls bypass CLI parsing. Many teams incorrectly assume command-line flags always win. Python’s standard configuration ecosystem evaluates sources in arbitrary orders. Reviewing Core Configuration Patterns & File Formats confirms that explicit precedence chains are mandatory for secure deployments.

Secure Implementation

Replace ad-hoc loading with a deterministic resolver. The following implementation enforces a strict CLI > Environment > .env > File > Defaults chain. It disables automatic dotenv overrides and maps CLI arguments explicitly. Type validation occurs at initialization to prevent malformed environment variable injection.

from pydantic_settings import BaseSettings, SettingsConfigDict
import argparse
import sys

def get_cli_overrides() -> dict[str, str]:
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument("--db-host", default=None)
    args, _ = parser.parse_known_args()
    return {k: v for k, v in vars(args).items() if v is not None}

class AppConfig(BaseSettings):
    db_host: str
    model_config = SettingsConfigDict(
        env_file=".env",
        env_file_encoding="utf-8",
        extra="ignore",
    )

    def __init__(self, **kwargs: object) -> None:
        super().__init__(**kwargs)
        cli_overrides = get_cli_overrides()
        for key, value in cli_overrides.items():
            setattr(self, key, value)

config = AppConfig()

Security & Type Safety Boundaries

Security boundaries require strict separation between configuration resolution and secret exposure. Never log resolved values or dump the entire settings object. Use runtime secret injection via AWS Secrets Manager or HashiCorp Vault. Validate all types at startup to reject malformed strings before they reach database drivers.

Validation & Environment Parity

Validation must guarantee environment parity between local development and production clusters. Unit tests should mock os.environ and assert CLI flags override injected variables. Integration tests require running containers with simulated Kubernetes ConfigMap injections. Verify the /health endpoint returns correct host metadata under load.

Maintain parity by mirroring deployment injection orders in CI/CD pipelines. Use pytest with monkeypatch to simulate exact precedence chains. Deploy to staging with a --dry-run flag that outputs a resolved configuration hash. This hash validates logic without exposing sensitive secrets in pipeline logs.

Prevention Strategies

Prevention strategies must enforce deterministic behavior across the platform. Adopt a centralized schema using Pydantic or Cerberus to document precedence explicitly. Implement a startup audit hook that logs warnings when multiple sources define identical keys. Disable override=True in .env loaders for production execution paths.

Bake non-secret configuration into immutable container images. Inject runtime secrets exclusively via sidecar proxies or ephemeral memory mounts. Enforce CI linting rules that flag unsafe dotenv configurations. These controls eliminate silent overrides and guarantee predictable service behavior.