Python Configuration ā Settings & Environment
Configuration management keeps your app flexible across environments (development, staging, production). Hardcoding values creates security risks and deployment headaches.
Learning Objectives
- Use environment variables for configuration
- Load .env files with python-dotenv
- Create typed configuration classes
- Manage secrets and feature flags
Why Configuration Management Matters
Development Staging Production
āāāāāāāāāāā āāāāāāā āāāāāāāāāā
DEBUG=True DEBUG=False DEBUG=False
DB=localhost DB=staging-db DB=production-db
PORT=8000 PORT=80 PORT=80
SECRET=dev-key SECRET=staging-key SECRET=prod-key
Without proper configuration, you'd need to change code for each environment ā dangerous and error-prone.
Environment Variables
import os
# Read from environment (set externally)
DATABASE_URL = os.environ.get("DATABASE_URL", "sqlite:///default.db")
DEBUG = os.environ.get("DEBUG", "false").lower() == "true"
PORT = int(os.environ.get("PORT", "8000"))
SECRET_KEY = os.environ.get("SECRET_KEY")
# Validate required variables
if not SECRET_KEY:
raise ValueError("SECRET_KEY environment variable is required")
# Type conversion
MAX_CONNECTIONS = int(os.environ.get("MAX_CONNECTIONS", "10"))
TIMEOUT = float(os.environ.get("TIMEOUT", "30.0"))
.env Files
# .env file (DO NOT commit to git!)
DATABASE_URL=postgresql://user:pass@localhost/mydb
DEBUG=true
SECRET_KEY=your-secret-key-here
PORT=8000
REDIS_URL=redis://localhost:6379
# Load .env into environment
from dotenv import load_dotenv
import os
load_dotenv() # Loads .env into os.environ
# Now accessible via os.environ
DATABASE_URL = os.environ["DATABASE_URL"]
DEBUG = os.environ.get("DEBUG", "false").lower() == "true"
Typed Configuration Class
import os
from dataclasses import dataclass, field
from typing import List
@dataclass
class Settings:
# Required (no default)
database_url: str
secret_key: str
# Optional with defaults
debug: bool = False
port: int = 8000
host: str = "0.0.0.0"
allowed_origins: List[str] = field(default_factory=lambda: ["http://localhost:3000"])
max_connections: int = 10
timeout: float = 30.0
@classmethod
def from_env(cls):
"""Create settings from environment variables."""
return cls(
database_url=os.environ.get("DATABASE_URL", ""),
secret_key=os.environ.get("SECRET_KEY", ""),
debug=os.environ.get("DEBUG", "false").lower() == "true",
port=int(os.environ.get("PORT", "8000")),
host=os.environ.get("HOST", "0.0.0.0"),
allowed_origins=os.environ.get("ALLOWED_ORIGINS", "").split(","),
max_connections=int(os.environ.get("MAX_CONNECTIONS", "10")),
timeout=float(os.environ.get("TIMEOUT", "30.0")),
)
def is_production(self) -> bool:
return not self.debug
# Usage
settings = Settings.from_env()
print(settings.database_url)
print(settings.is_production())
Environment-Specific Config
import os
class Config:
def __init__(self):
self.env = os.environ.get("APP_ENV", "development")
@property
def database_url(self):
if self.env == "production":
return os.environ["DATABASE_URL"]
elif self.env == "staging":
return os.environ.get("DATABASE_URL", "sqlite:///staging.db")
else:
return os.environ.get("DATABASE_URL", "sqlite:///dev.db")
@property
def debug(self):
return self.env != "production"
Key Takeaways
- Never hardcode configuration values in source code
- Use .env files for local development (add to .gitignore!)
- Use environment variables in production
- Create typed config classes for validation and IDE support
- Validate required configuration at startup
- Keep secrets out of version control
- Use different configs for dev/staging/production
- Document required environment variables in README