Python
best practices
configuration files
settings management
software development

What's the best practice using a settingsconfig file in Python?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

A Python application usually needs configuration for things like ports, database URLs, feature flags, and API keys. The best practice is not to put every setting into one ad hoc module, but to separate defaults, environment-specific overrides, and secrets so the program can run consistently in development, test, and production.

Keep Configuration Outside Business Logic

The first rule is simple: code should read configuration, not contain environment-specific decisions scattered everywhere. If a value changes between machines or deployments, it belongs in configuration.

A practical setup is:

  • checked-in defaults in a file such as config.toml
  • environment variables for secrets and deployment overrides
  • one loader module that validates and exposes typed settings

That gives you one place to reason about configuration instead of many unrelated os.getenv calls.

A Simple Typed Loader

Python 3.11 includes tomllib, which makes TOML a good default for human-edited settings.

python
1from dataclasses import dataclass
2import os
3import tomllib
4
5@dataclass
6class Settings:
7    debug: bool
8    host: str
9    port: int
10    database_url: str
11
12
13def load_settings(path: str = "config.toml") -> Settings:
14    with open(path, "rb") as f:
15        raw = tomllib.load(f)
16
17    app = raw["app"]
18    db = raw["database"]
19
20    return Settings(
21        debug=bool(app.get("debug", False)),
22        host=os.getenv("APP_HOST", app.get("host", "127.0.0.1")),
23        port=int(os.getenv("APP_PORT", app.get("port", 8000))),
24        database_url=os.getenv("DATABASE_URL", db["url"]),
25    )
26
27
28if __name__ == "__main__":
29    settings = load_settings()
30    print(settings)

A matching config.toml can look like this:

toml
1[app]
2debug = true
3host = "127.0.0.1"
4port = 8000
5
6[database]
7url = "sqlite:///local.db"

This pattern is boring in a good way. It is easy to inspect, easy to test, and easy to override when deployment needs a different host or database URL.

Use Environment Variables for Secrets

Secrets should usually not live in a checked-in config file. Database passwords, JWT secrets, and third-party tokens should come from environment variables or a secrets manager.

That does not mean every setting should be an environment variable. A better split is:

  • stable non-secret defaults in a config file
  • secret or deployment-specific values in the environment
  • validation at startup so failures happen early

If a required secret is missing, fail immediately rather than continuing with an invalid default.

python
1import os
2
3api_key = os.getenv("PAYMENT_API_KEY")
4if not api_key:
5    raise RuntimeError("PAYMENT_API_KEY is required")

Make Test and Production Behavior Explicit

One common mistake is implicitly switching behavior based on file presence or machine name. That makes tests unpredictable. Instead, choose a clear configuration source order and document it.

A common order is:

  1. built-in defaults
  2. config file values
  3. environment variable overrides

That order makes local development pleasant while still letting containers, CI jobs, and cloud platforms inject the final production values.

If your application grows, libraries such as pydantic-settings can add stricter validation and parsing, but the core idea stays the same: one settings object, one loading path, predictable override rules.

Organize by Concern, Not by Random Names

Avoid a giant file with unrelated top-level keys. Group settings by concern so readers can understand what belongs together.

Good groups include:

  • 'app for host, port, debug, logging level'
  • 'database for connection details'
  • 'cache for Redis or in-memory cache settings'
  • 'auth for issuer URLs and token parameters'

That organization matters more over time than the exact file format.

Common Pitfalls

  • Importing configuration at module import time and making tests hard to isolate.
  • Storing secrets in version-controlled config files.
  • Mixing os.getenv calls throughout the codebase instead of centralizing loading.
  • Using silent fallbacks for required settings and discovering the problem only after deployment.
  • Creating separate config styles for each environment instead of using one format with explicit override rules.

Summary

  • Keep environment-specific values out of business logic.
  • Use a checked-in config file for defaults and environment variables for secrets or deployment overrides.
  • Expose settings through one typed loader instead of scattered lookups.
  • Validate required values at startup so configuration errors fail fast.
  • Prefer boring, explicit configuration rules over clever auto-detection.

Course illustration
Course illustration

All Rights Reserved.