Python Configuration Hierarchy — Core Concepts

The Problem with Flat Configuration

Early in a project, settings live in a single file — config.py with hardcoded values. This works until you need different settings for development, staging, and production. Suddenly you’re editing code for each environment, or worse, checking production secrets into version control.

A configuration hierarchy solves this by separating what your app can be configured with from where those values come from.

The Standard Layer Order

Most production Python applications follow this precedence (highest wins):

PrioritySourceExample
1 (highest)Command-line arguments--port 9000
2Environment variablesAPP_PORT=9000
3Local config file.env.local or config.local.yaml
4Environment-specific fileconfig.production.yaml
5Shared config fileconfig.yaml
6 (lowest)Hardcoded defaultsDEFAULT_PORT = 8000

This follows the Twelve-Factor App principle: environment-specific values come from the environment, not from code.

Why This Order?

Defaults exist so the app always starts, even with zero configuration. A developer clones the repo and runs it — defaults provide a working local setup.

Config files let you define structured settings (nested values, lists, comments) that are version-controlled and reviewable.

Environment variables are the standard interface between your app and its deployment platform. Kubernetes, Docker, Heroku, and every CI system set them natively. They override files because the deployment environment knows things the code doesn’t (like which database to connect to).

CLI arguments are the escape hatch — they let an operator override anything at runtime without modifying files or environment.

Merging Strategies

When config sources overlap, you need clear merge rules:

Shallow merge replaces entire keys. If the file says database: {host: localhost, port: 5432} and the environment says DATABASE_HOST=prod.db.com, you get {host: prod.db.com, port: 5432} — the environment overrides the specific key.

Deep merge combines nested structures. If one layer defines logging.level = DEBUG and another defines logging.format = json, the merged result has both.

Most production systems use shallow merge for simplicity and predictability. Deep merge can produce surprising results when you don’t realize which layer contributed which nested key.

Environment Variable Naming Conventions

Since environment variables are flat strings, you need a convention for mapping them to structured config:

  • APP_DATABASE_HOSTdatabase.host
  • APP_REDIS_URLredis.url
  • Double underscores for nesting: APP__DATABASE__HOST

The prefix (APP_) prevents collisions with system variables.

The Secrets Layer

Secrets (API keys, database passwords) deserve special treatment. They should never appear in config files or version control. Common approaches:

  • Environment variables (simple but visible in process lists)
  • Secret management services (AWS Secrets Manager, HashiCorp Vault)
  • Kubernetes Secrets mounted as files

Many teams add a separate secrets layer that overrides config files but sits alongside environment variables.

Common Misconception

“Just use environment variables for everything.” Environment variables work well for simple key-value pairs, but they’re awkward for complex structures (lists, nested objects) and don’t support comments or documentation. A config file with environment variable overrides gives you the best of both worlds: structured, documented defaults that can be overridden at deploy time.

One thing to remember: Stack your configuration in predictable layers — defaults at the bottom, CLI at the top — and be explicit about which layer wins when values conflict.

pythonconfigurationproduction

See Also