python-decouple separates config from code. pip install python-decouple. Read: from decouple import config. SECRET_KEY = config("SECRET_KEY"). DEBUG = config("DEBUG", default=False, cast=bool). PORT = config("PORT", default=8000, cast=int). Sources: reads from .env file first, then environment variables (env vars override .env). Required: calling config("KEY") with no default raises UndefinedValueError if unset. Bool cast: "true", "True", "1", "yes" → True. "false", "0", "no" → False. Csv: from decouple import Csv. ALLOWED_HOSTS = config("ALLOWED_HOSTS", default="localhost", cast=Csv()). ALLOWED_HOSTS = config("ALLOWED_HOSTS", cast=Csv(cast=str, delimiter=",", strip=" ")). Choices: from decouple import Choices. ENV = config("APP_ENV", cast=Choices(["dev","staging","prod"])) → raises ValueError if not in set. Explicit file: from decouple import RepositoryEnv; config = Config(RepositoryEnv(".env.prod")). Auto: from decouple import AutoConfig. config = AutoConfig() — searches up the directory tree for .env. Ini: also reads from .ini with [settings] section. Comment: .env supports # comments and blank lines. Override: env var takes precedence over .env. os.environ["KEY"] in tests. Claude Code generates decouple config modules, .env templates, and settings classes.
CLAUDE.md for python-decouple
## python-decouple Stack
- Version: python-decouple >= 3.8 | pip install python-decouple
- Read: config("KEY") — reads .env then os.environ; no default → required
- Types: config("PORT", cast=int) | config("DEBUG", cast=bool) | Csv() for lists
- Default: config("KEY", default="val") — optional setting with fallback
- Choices: config("ENV", cast=Choices(["dev","staging","prod"])) — validated enum
- File: Config(RepositoryEnv(".env.prod")) — explicit .env file path
- Override: env vars always override .env — safe for CI/CD secrets injection
python-decouple Configuration Pipeline
# app/settings.py — python-decouple config module
from __future__ import annotations
from pathlib import Path
from decouple import AutoConfig, Choices, Config, Csv, RepositoryEnv, UndefinedValueError, config
# ─────────────────────────────────────────────────────────────────────────────
# 1. Application settings — single module pattern
# ─────────────────────────────────────────────────────────────────────────────
# Core
SECRET_KEY: str = config("SECRET_KEY") # required
DEBUG: bool = config("DEBUG", default=False, cast=bool)
APP_ENV: str = config("APP_ENV", default="development",
cast=Choices(["development","staging","production"]))
# Server
HOST: str = config("HOST", default="0.0.0.0")
PORT: int = config("PORT", default=8000, cast=int)
WORKERS: int = config("WORKERS", default=1, cast=int)
RELOAD: bool = config("RELOAD", default=DEBUG, cast=bool)
# Database
DATABASE_URL: str = config("DATABASE_URL", default="sqlite:///./dev.db")
DB_POOL_SIZE: int = config("DB_POOL_SIZE", default=5, cast=int)
DB_MAX_OVERFLOW: int = config("DB_MAX_OVERFLOW", default=10, cast=int)
DB_ECHO: bool = config("DB_ECHO", default=False, cast=bool)
# Redis
REDIS_URL: str = config("REDIS_URL", default="redis://localhost:6379/0")
CACHE_TTL: int = config("CACHE_TTL", default=300, cast=int)
# Security
ALLOWED_HOSTS: list = config("ALLOWED_HOSTS", default="localhost,127.0.0.1",
cast=Csv())
CORS_ORIGINS: list = config("CORS_ORIGINS", default="http://localhost:3000",
cast=Csv())
JWT_ALGORITHM: str = config("JWT_ALGORITHM", default="HS256")
ACCESS_TOKEN_EXPIRE_MINUTES: int = config("ACCESS_TOKEN_EXPIRE_MINUTES",
default=30, cast=int)
# Email
SMTP_HOST: str = config("SMTP_HOST", default="localhost")
SMTP_PORT: int = config("SMTP_PORT", default=587, cast=int)
SMTP_TLS: bool = config("SMTP_TLS", default=True, cast=bool)
SMTP_USER: str = config("SMTP_USER", default="")
SMTP_PASSWORD: str = config("SMTP_PASSWORD", default="")
EMAIL_FROM: str = config("EMAIL_FROM", default="[email protected]")
# External APIs
STRIPE_SECRET_KEY: str = config("STRIPE_SECRET_KEY", default="")
SENDGRID_API_KEY: str = config("SENDGRID_API_KEY", default="")
S3_BUCKET: str = config("S3_BUCKET", default="")
AWS_REGION: str = config("AWS_REGION", default="us-east-1")
# Feature flags
FEATURE_SIGNUP: bool = config("FEATURE_SIGNUP", default=True, cast=bool)
FEATURE_PAYMENTS: bool = config("FEATURE_PAYMENTS", default=False, cast=bool)
# Logging
LOG_LEVEL: str = config("LOG_LEVEL", default="INFO",
cast=Choices(["DEBUG","INFO","WARNING","ERROR","CRITICAL"]))
LOG_FORMAT: str = config("LOG_FORMAT", default="json",
cast=Choices(["json","text"]))
# ─────────────────────────────────────────────────────────────────────────────
# 2. Environment-specific .env files
# ─────────────────────────────────────────────────────────────────────────────
def load_env_config(env: str | None = None) -> Config:
"""
Load config from a specific .env.{env} file.
Useful for running: python -c "from settings import load_env_config; cfg = load_env_config('staging')"
"""
target_env = env or APP_ENV
env_file = Path(f".env.{target_env}")
if env_file.exists():
return Config(RepositoryEnv(str(env_file)))
# Fall back to .env or environment variables only
return Config(RepositoryEnv(".env")) if Path(".env").exists() else Config({})
# ─────────────────────────────────────────────────────────────────────────────
# 3. Settings validation at startup
# ─────────────────────────────────────────────────────────────────────────────
def validate_settings() -> list[str]:
"""
Check that all required settings are present and valid.
Call at application startup to fail fast on misconfiguration.
"""
errors: list[str] = []
if not SECRET_KEY or len(SECRET_KEY) < 32:
errors.append("SECRET_KEY must be at least 32 characters")
if APP_ENV == "production":
if DEBUG:
errors.append("DEBUG must be False in production")
if not SMTP_USER or not SMTP_PASSWORD:
errors.append("SMTP_USER and SMTP_PASSWORD required in production")
if not DATABASE_URL.startswith("postgresql"):
errors.append("DATABASE_URL must be PostgreSQL in production")
return errors
def assert_settings_valid() -> None:
"""Raise RuntimeError if any settings are invalid — call at app startup."""
errors = validate_settings()
if errors:
raise RuntimeError("Configuration errors:\n" + "\n".join(f" - {e}" for e in errors))
# ─────────────────────────────────────────────────────────────────────────────
# 4. FastAPI settings integration
# ─────────────────────────────────────────────────────────────────────────────
try:
from fastapi import FastAPI
def create_app() -> FastAPI:
assert_settings_valid()
return FastAPI(
title="My API",
debug=DEBUG,
docs_url="/docs" if DEBUG else None, # hide docs in prod
)
except ImportError:
pass
# ─────────────────────────────────────────────────────────────────────────────
# 5. Django settings integration
# ─────────────────────────────────────────────────────────────────────────────
# Example: paste into Django's settings.py
DJANGO_SETTINGS_EXAMPLE = """
from decouple import config, Csv
SECRET_KEY = config("SECRET_KEY")
DEBUG = config("DEBUG", default=False, cast=bool)
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": config("DB_NAME", default="myapp"),
"USER": config("DB_USER", default="postgres"),
"PASSWORD": config("DB_PASSWORD", default=""),
"HOST": config("DB_HOST", default="localhost"),
"PORT": config("DB_PORT", default="5432"),
}
}
ALLOWED_HOSTS = config("ALLOWED_HOSTS", default="localhost", cast=Csv())
"""
# ─────────────────────────────────────────────────────────────────────────────
# 6. .env file template
# ─────────────────────────────────────────────────────────────────────────────
ENV_TEMPLATE = """\
# ── Required ─────────────────────────────────────────────
SECRET_KEY=change-me-to-a-long-random-string-at-least-32-chars
# ── Application ──────────────────────────────────────────
DEBUG=true
APP_ENV=development
HOST=0.0.0.0
PORT=8000
WORKERS=1
# ── Database ─────────────────────────────────────────────
DATABASE_URL=postgresql://postgres:password@localhost/myapp_dev
DB_POOL_SIZE=5
DB_MAX_OVERFLOW=10
DB_ECHO=false
# ── Redis ────────────────────────────────────────────────
REDIS_URL=redis://localhost:6379/0
CACHE_TTL=300
# ── Security ─────────────────────────────────────────────
ALLOWED_HOSTS=localhost,127.0.0.1
CORS_ORIGINS=http://localhost:3000
JWT_ALGORITHM=HS256
ACCESS_TOKEN_EXPIRE_MINUTES=30
# ── Email ────────────────────────────────────────────────
SMTP_HOST=localhost
SMTP_PORT=587
SMTP_TLS=true
SMTP_USER=
SMTP_PASSWORD=
[email protected]
# ── Feature flags ────────────────────────────────────────
FEATURE_SIGNUP=true
FEATURE_PAYMENTS=false
# ── Logging ──────────────────────────────────────────────
LOG_LEVEL=DEBUG
LOG_FORMAT=text
"""
def write_env_template(path: str = ".env.example") -> None:
"""Write a .env.example template — commit this to version control."""
Path(path).write_text(ENV_TEMPLATE)
print(f"Wrote {path}")
# ─────────────────────────────────────────────────────────────────────────────
# Demo
# ─────────────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
import os
# Simulate environment for demo
os.environ.setdefault("SECRET_KEY", "demo-secret-key-that-is-at-least-32-chars!!")
os.environ.setdefault("DATABASE_URL", "sqlite:///./demo.db")
# Reload config values after setting env vars
from decouple import config as _c
print(f"DEBUG: {DEBUG}")
print(f"APP_ENV: {APP_ENV}")
print(f"PORT: {PORT}")
print(f"ALLOWED_HOSTS: {ALLOWED_HOSTS}")
print(f"LOG_LEVEL: {LOG_LEVEL}")
errors = validate_settings()
print(f"\nValidation: {'PASS' if not errors else 'FAIL'}")
for e in errors:
print(f" - {e}")
write_env_template("/tmp/.env.example")
For the os.environ.get alternative — os.environ.get("SECRET_KEY", "") returns a raw string regardless of the intended type, silently uses the empty default if the variable is missing (no UndefinedValueError), requires manual int() / bool() casting, and does not read .env files, while config("SECRET_KEY") raises UndefinedValueError immediately if the variable is missing and no default is provided, cast=bool converts "true"/"1"/"yes" to True and "false"/"0"/"no" to False, cast=Csv() splits comma-separated strings into a list, and the value is read from .env first then from os.environ so a CI environment variable always overrides the file. For the pydantic-settings / BaseSettings alternative — pydantic-settings validates the full settings schema with type annotations and @validator decorators, making it the right choice when settings are complex models with cross-field validation, while python-decouple has zero transitive dependencies, reads .env, .ini, and os.environ with a single config() call, and is several times simpler to set up for projects that just need typed environment variable access without a full Pydantic model. The Claude Skills 360 bundle includes python-decouple skill sets covering config with required and default values, cast=int/bool/Csv for type conversion, Choices for validated enum values, UndefinedValueError on missing required settings, RepositoryEnv for explicit .env file selection, AutoConfig for directory-traversal discovery, .env template generation, startup validation, Django DATABASES and ALLOWED_HOSTS patterns, and FastAPI settings module integration. Start with the free tier to try environment configuration code generation.