Dynaconf manages layered application settings. pip install dynaconf. Init: from dynaconf import Dynaconf. settings = Dynaconf(settings_file=["settings.toml",".secrets.toml"], envvar_prefix="MYAPP", environments=True). Access: settings.DATABASE_URL. settings.DEBUG. settings.get("PORT", 8000). Override with env: MYAPP_DEBUG=true python app.py. Environments: ENV_FOR_DYNACONF=production python app.py. settings.toml: [default] DEBUG=false DB_URL="sqlite:///dev.db". [production] DEBUG=false DB_URL="postgresql://...". .secrets.toml: [default] SECRET_KEY="dev-secret". [production] SECRET_KEY="prod-secret" — add to .gitignore. Validators: from dynaconf import Validator. settings = Dynaconf(validators=[Validator("SECRET_KEY", must_exist=True, len_min=32)]). settings.validators.validate(). Type: Validator("PORT", cast=int, default=8000). Choice: Validator("APP_ENV", is_in=["dev","staging","prod"]). Merge dicts: MERGE_ENABLED_FOR_DYNACONF=true or @merge prefix in toml. Populate obj: settings.populate_obj(app.config) — Flask dict-style config. Flask ext: from dynaconf.utils.functional import DynaBox. Django ext: pip install django-dynaconf. from dynaconf.contrib import DjangoDynaconf. CLI: dynaconf list — shows all settings. dynaconf validate. Claude Code generates Dynaconf settings.toml configs, validators, and Flask/FastAPI integration.
CLAUDE.md for Dynaconf
## Dynaconf Stack
- Version: dynaconf >= 3.2 | pip install dynaconf
- Init: Dynaconf(settings_file=["settings.toml",".secrets.toml"], envvar_prefix="APP")
- Access: settings.DATABASE_URL | settings.get("PORT", 8000)
- Override: APP_DATABASE_URL=... python app.py (prefix + uppercase key)
- Env: ENV_FOR_DYNACONF=production python app.py — switches to [production] section
- Validate: Validator("SECRET_KEY", must_exist=True, len_min=32)
- Secrets: .secrets.toml — keep out of version control, override sensitive keys
Dynaconf Settings Pipeline
# app/config.py — Dynaconf layered settings with validators
from __future__ import annotations
from pathlib import Path
from dynaconf import Dynaconf, Validator
# ─────────────────────────────────────────────────────────────────────────────
# Settings instance — reads settings.toml, .secrets.toml, then env vars
# ─────────────────────────────────────────────────────────────────────────────
settings = Dynaconf(
# Files to read — later files override earlier ones
settings_file=["settings.toml", ".secrets.toml"],
# Environment variable prefix: APP_DEBUG=true overrides settings.DEBUG
envvar_prefix="APP",
# Enable [default] / [development] / [production] sections
environments=True,
# Load .env file
load_dotenv=True,
dotenv_path=".env",
# Lowercase keys in access — settings.debug and settings.DEBUG both work
lowercase_read=True,
# Validators run on first access
validators=[
# Required keys
Validator("secret_key", must_exist=True),
Validator("database_url", must_exist=True),
# Type and range validation
Validator("port", cast=int, default=8000,
gte=1, lte=65535),
Validator("workers", cast=int, default=1, gte=1),
Validator("cache_ttl", cast=int, default=300, gte=0),
Validator("debug", cast=bool, default=False),
# Enum validation
Validator("app_env", must_exist=True,
is_in=["development", "staging", "production"]),
Validator("log_level", default="INFO",
is_in=["DEBUG","INFO","WARNING","ERROR","CRITICAL"]),
# Length validation
Validator("secret_key", len_min=32,
messages={"len_min": "SECRET_KEY must be >= 32 characters"}),
],
)
# ─────────────────────────────────────────────────────────────────────────────
# settings.toml — checked into version control (no secrets)
# ─────────────────────────────────────────────────────────────────────────────
SETTINGS_TOML = """\
[default]
app_env = "development"
debug = true
host = "0.0.0.0"
port = 8000
workers = 1
log_level = "DEBUG"
log_format = "text"
cache_ttl = 300
database_url = "sqlite:///./dev.db"
redis_url = "redis://localhost:6379/0"
allowed_hosts = ["localhost","127.0.0.1"]
smtp_host = "localhost"
smtp_port = 587
smtp_tls = true
[staging]
debug = false
app_env = "staging"
log_level = "INFO"
log_format = "json"
workers = 2
database_url = "postgresql://postgres:@staging-db/myapp"
[production]
debug = false
app_env = "production"
log_level = "INFO"
log_format = "json"
workers = 4
allowed_hosts = ["api.example.com","*.example.com"]
"""
# .secrets.toml — NOT committed to version control
SECRETS_TOML = """\
[default]
secret_key = "dev-secret-key-replace-in-production-32+"
[staging]
secret_key = "@format {env[STAGING_SECRET_KEY]}"
[production]
secret_key = "@format {env[PROD_SECRET_KEY]}"
smtp_user = "@format {env[SMTP_USER]}"
smtp_password = "@format {env[SMTP_PASSWORD]}"
"""
def write_config_files(base_dir: Path = Path(".")) -> None:
"""Write settings.toml and .secrets.toml template files."""
(base_dir / "settings.toml").write_text(SETTINGS_TOML)
secrets_path = base_dir / ".secrets.toml"
if not secrets_path.exists():
secrets_path.write_text(SECRETS_TOML)
print(f"Created {secrets_path} — add to .gitignore!")
print(f"Created {base_dir / 'settings.toml'}")
# ─────────────────────────────────────────────────────────────────────────────
# Settings access helpers
# ─────────────────────────────────────────────────────────────────────────────
def is_production() -> bool:
return settings.app_env == "production"
def is_debug() -> bool:
return settings.debug
def db_config() -> dict:
return {
"url": settings.database_url,
"pool_size": settings.get("db_pool_size", 5),
"max_overflow": settings.get("db_max_overflow", 10),
"echo": settings.get("db_echo", False),
}
def smtp_config() -> dict:
return {
"host": settings.smtp_host,
"port": settings.smtp_port,
"tls": settings.smtp_tls,
"user": settings.get("smtp_user", ""),
"password": settings.get("smtp_password", ""),
}
# ─────────────────────────────────────────────────────────────────────────────
# FastAPI integration
# ─────────────────────────────────────────────────────────────────────────────
try:
from fastapi import FastAPI
def create_app() -> FastAPI:
# Validate all settings on startup
settings.validators.validate()
return FastAPI(
title="My API",
debug=settings.debug,
docs_url="/docs" if settings.debug else None,
)
except ImportError:
pass
# ─────────────────────────────────────────────────────────────────────────────
# Flask integration
# ─────────────────────────────────────────────────────────────────────────────
FLASK_INTEGRATION_EXAMPLE = """
from flask import Flask
from dynaconf.utils.functional import DynaBox
def create_flask_app() -> Flask:
app = Flask(__name__)
# Merge Dynaconf into Flask config object
settings.populate_obj(app.config)
# Or set individual keys:
app.config["SECRET_KEY"] = settings.secret_key
app.config["DEBUG"] = settings.debug
return app
"""
# ─────────────────────────────────────────────────────────────────────────────
# Testing — override settings in tests
# ─────────────────────────────────────────────────────────────────────────────
TESTING_EXAMPLE = """
import pytest
from dynaconf import settings
@pytest.fixture(autouse=True)
def test_settings():
# Override settings for tests — changes are scoped to the with block
with settings.using_env("testing"):
settings.configure(
DATABASE_URL="sqlite:///:memory:",
DEBUG=True,
FEATURE_PAYMENTS=False,
)
yield
# Settings restored after yield
def test_db_url():
assert "memory" in settings.database_url
"""
# ─────────────────────────────────────────────────────────────────────────────
# Demo
# ─────────────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
import os, tempfile
with tempfile.TemporaryDirectory() as tmp:
base = Path(tmp)
write_config_files(base)
# Create a fresh settings instance pointing at the temp files
from dynaconf import Dynaconf
test_settings = Dynaconf(
settings_file=[str(base / "settings.toml")],
envvar_prefix="DEMO",
environments=True,
)
# Provide required secret via env
os.environ["DEMO_SECRET_KEY"] = "my-demo-secret-key-that-is-long-enough!!"
os.environ["DEMO_DATABASE_URL"] = "sqlite:///:memory:"
print(f"app_env: {test_settings.app_env}")
print(f"debug: {test_settings.debug}")
print(f"port: {test_settings.get('port', 8000)}")
print(f"log_level:{test_settings.get('log_level','INFO')}")
# Switch to production layer
os.environ["ENV_FOR_DYNACONF"] = "production"
from dynaconf import Dynaconf as D2
prod_settings = D2(
settings_file=[str(base / "settings.toml")],
envvar_prefix="DEMO",
environments=True,
)
print(f"\nProduction debug: {prod_settings.debug}")
print(f"Production workers:{prod_settings.get('workers', 1)}")
For the os.environ + configparser alternative — using configparser for layered [dev]/[prod] sections requires custom merging logic, has no built-in validator or secret-file support, and exposes everything as raw strings requiring manual casting, while Dynaconf reads .toml, .yaml, .json, or .ini files, resolves @format {env[VAR]} interpolations for secrets, merges environment layers automatically when ENV_FOR_DYNACONF=production, validates with Validator("KEY", must_exist=True, len_min=32), and exposes settings.port as a typed integer. For the python-decouple alternative — python-decouple reads a single .env file per process invocation and applies one flat set of values, while Dynaconf stacks multiple files (settings.toml for checked-in defaults, .secrets.toml for local secrets, then environment variables) and switches environment layers without restarting the process — settings.using_env("testing") scopes overrides to a with block, making Dynaconf the right choice when the same app runs in multiple environments and settings must be merged, not just overridden. The Claude Skills 360 bundle includes Dynaconf skill sets covering Dynaconf initialization with settings_file and envvar_prefix, TOML settings.toml with default/staging/production sections, .secrets.toml with @format env interpolations, Validator for must_exist/len_min/is_in/gte/cast, environments=True layer switching, settings.using_env for test isolation, populate_obj for Flask config dict merge, FastAPI startup validation, dynaconf list CLI for debugging, and Docker/k8s environment variable injection patterns. Start with the free tier to try multi-environment settings code generation.