A production Python project juggles several kinds of configuration: database connections, API keys, model hyperparameters, and different environments (development, testing, production). This is how I think about the trade-offs.
1. Environment variables (the baseline)
Read with os.environ. Simple, but weakly typed and awkward to manage.
import os
DB_URL = os.environ.get("DATABASE_URL", "sqlite:///dev.db")
2. .env files + python-dotenv
Put environment variables in .env so switching is easy during development.
Always add .env to .gitignore.
# .env
DATABASE_URL=postgres://...
SECRET_KEY=xxx
3. YAML / JSON / TOML config files
Good for structured configuration, such as machine-learning hyperparameters.
# config.yaml
model:
name: resnet50
lr: 0.001
batch_size: 64
data:
train: data/train
val: data/val
4. Pydantic Settings (recommended)
Type safety, automatic validation, and merging across sources (env > file > default). My default choice for new projects.
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
db_url: str
secret_key: str
debug: bool = False
class Config:
env_file = ".env"
settings = Settings()
5. Secrets
Never put secrets in code, config files, or git. Team projects should use
Vault / AWS Secrets Manager / GCP Secret Manager; personal projects
should at least use .env plus .gitignore.
If a secret was ever committed, rotate it immediately and clean up the git history.
What I do in practice
- Small scripts:
os.environplus.env. - Medium projects: Pydantic Settings plus
.env. - Training runs:
HydraorOmegaConffor composing configs. - Deployment: 12-factor, all configuration via environment variables, secrets handled by the cloud provider.