Python Dynaconf Settings — Deep Dive
System design lens
Configuration management in production systems requires balancing developer convenience with operational safety. Dynaconf’s architecture addresses this through a layered loading pipeline, pluggable backends, and runtime validation — making it suitable for applications that outgrow simple environment variables but don’t need the composition complexity of Hydra.
Internal loading pipeline
When Dynaconf initializes, it processes sources in this order:
1. Coded defaults (Dynaconf constructor kwargs)
2. settings_files (first to last)
3. Environment-specific sections from those files
4. .secrets files (same environment logic)
5. External loaders (Vault, Redis, custom)
6. Environment variables (DYNACONF_* prefix)
7. Runtime set() calls
Each layer merges into the settings namespace. For nested structures, Dynaconf performs deep merging by default:
[default.database]
host = "localhost"
port = 5432
pool_size = 5
[production.database]
host = "prod-db.internal"
pool_size = 20
# port inherits from default: 5432
The dynaconf_merge key provides explicit control over merge behavior for complex nested structures.
Box-based settings object
Dynaconf wraps settings in a Box object that provides attribute-style and dict-style access:
# All equivalent
settings.DATABASE.HOST
settings["DATABASE"]["HOST"]
settings.get("DATABASE.HOST")
settings("DATABASE__HOST", default="localhost") # Double underscore for nesting
The double-underscore convention extends to environment variables:
export DYNACONF_DATABASE__HOST="prod-server"
export DYNACONF_DATABASE__POOL__MAX_SIZE="@int 50"
HashiCorp Vault integration
For production secrets management, Dynaconf integrates with HashiCorp Vault:
settings = Dynaconf(
settings_files=["settings.toml"],
vault_enabled=True,
vault_url="https://vault.company.com",
vault_token=os.environ.get("VAULT_TOKEN"),
vault_path="secret/data/myapp",
vault_mount_point="secret",
)
Environment-specific vault paths allow different secrets per environment:
# Reads from secret/data/myapp/production in production
# Reads from secret/data/myapp/development in development
Vault values take precedence over file-based settings but are overridden by environment variables, maintaining the escape-hatch pattern.
Redis as a configuration backend
For distributed applications that need synchronized configuration:
settings = Dynaconf(
redis_enabled=True,
redis={
"host": "config-redis.internal",
"port": 6379,
"db": 0,
},
)
Writing configuration to Redis:
from dynaconf.loaders import redis_loader
redis_loader.write(settings, {"FEATURE_NEW_UI": True, "RATE_LIMIT": 1000})
All application instances read from the same Redis, enabling real-time configuration propagation across a fleet of servers.
Custom loaders
Build loaders for any backend — databases, remote APIs, Consul:
# myloaders/consul_loader.py
import consul
def load(obj, env=None, silent=True, key=None, filename=None):
"""Load settings from Consul KV store."""
client = consul.Consul(host=obj.get("CONSUL_HOST", "localhost"))
prefix = f"myapp/{env or 'default'}/"
_, entries = client.kv.get(prefix, recurse=True) or (None, [])
if entries:
for entry in entries:
key = entry["Key"].replace(prefix, "").upper().replace("/", "__")
value = entry["Value"].decode() if entry["Value"] else ""
obj.set(key, value)
Register the loader:
settings = Dynaconf(
loaders=["myloaders.consul_loader", "dynaconf.loaders.env_loader"],
)
Advanced validation patterns
Validation can express complex business rules:
from dynaconf import Validator
validators = [
# Conditional requirements
Validator("SMTP_PASSWORD", must_exist=True,
when=Validator("EMAIL_BACKEND", eq="smtp")),
# Cross-field validation
Validator("POOL_MAX", gte=Validator("POOL_MIN")),
# Environment-specific rules
Validator("DEBUG", eq=False,
env="production",
messages={"operations": "DEBUG must be False in production"}),
# Compound conditions
Validator("SSL_CERT", "SSL_KEY", must_exist=True,
when=Validator("USE_SSL", eq=True)),
# Type and range
Validator("WORKERS", is_type_of=int, gte=1, lte=32),
Validator("LOG_LEVEL", is_in=["DEBUG", "INFO", "WARNING", "ERROR"]),
]
Secrets file management
The .secrets file pattern keeps sensitive data separate:
# .secrets.toml (gitignored)
[default]
secret_key = "dev-secret-not-actually-secret"
api_key = "dev-api-key"
[production]
secret_key = "@vault secret/data/myapp/secret_key"
api_key = "@vault secret/data/myapp/api_key"
The @vault marker tells Dynaconf to fetch the value from Vault rather than using the literal string.
Django integration deep dive
# settings.py
import dynaconf
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"myapp",
]
# Dynaconf takes over after this point
settings = dynaconf.DjangoDynaconf(
__name__,
settings_files=["settings.toml", ".secrets.toml"],
environments=True,
env_switcher="DJANGO_ENV",
validators=[
dynaconf.Validator("SECRET_KEY", must_exist=True, min_len=32),
dynaconf.Validator("ALLOWED_HOSTS", must_exist=True, env="production"),
],
)
Django views and models access settings normally through django.conf.settings. Dynaconf intercepts attribute access and serves values from its layered sources.
Testing strategies
Isolated test settings
import pytest
from dynaconf import Dynaconf
@pytest.fixture
def settings():
return Dynaconf(
settings_files=["tests/settings.toml"],
environments=True,
env="testing",
)
def test_debug_disabled_in_production():
s = Dynaconf(
settings_files=["settings.toml"],
environments=True,
env="production",
)
assert s.DEBUG is False
Using dynaconf’s test utilities
from dynaconf.utils import parse_conf
def test_type_casting():
assert parse_conf("@bool true") is True
assert parse_conf("@int 42") == 42
assert parse_conf("@json [1,2,3]") == [1, 2, 3]
Temporary overrides in tests
def test_feature_flag():
with settings.using_env("testing"):
settings.set("FEATURE_FLAG", True)
assert run_feature() == "new_behavior"
# Original settings restored after context exit
Performance considerations
Dynaconf caches resolved settings after first access. The initial load parses files and queries external sources, but subsequent reads are dictionary lookups — effectively free.
For applications with hundreds of settings across multiple files, startup time is typically under 100ms. External loaders (Vault, Redis) add network latency to the first load but are cached afterward unless reload() is called.
Lazy loading pattern
settings = Dynaconf(
settings_files=["settings.toml"],
lazy_load=True, # Don't load files until first access
)
# Files not yet parsed
# First access triggers loading
db_url = settings.DATABASE_URL # Now files are parsed
Comparison with alternatives
| Feature | python-dotenv | Dynaconf | Hydra |
|---|---|---|---|
| Multi-format support | .env only | TOML, YAML, JSON, INI, .env | YAML only |
| Environment switching | Manual file selection | Built-in sections | Config groups |
| Validation | None | Built-in validators | Via structured configs |
| Secrets management | None | Vault, Redis built-in | Manual |
| Live reloading | No | Yes | No |
| CLI overrides | No | Limited | Full command-line |
| Composition | No | Layered merging | Hierarchical groups |
| Best for | Simple projects | Web apps, services | ML experiments |
One thing to remember
Dynaconf occupies the middle ground between simple dotenv files and complex composition frameworks. It excels when you need multi-environment support, secrets integration, and runtime validation for web applications and services — delivering enterprise configuration patterns without enterprise complexity.
See Also
- Python Black Formatter Understand Black Formatter through a practical analogy so your Python decisions become faster and clearer.
- Python Bumpversion Release Change your software's version number in every file at once with a single command — no more find-and-replace mistakes.
- Python Changelog Automation Let your git commits write the changelog so you never forget what changed in a release.
- Python Ci Cd Python Understand CI CD Python through a practical analogy so your Python decisions become faster and clearer.
- Python Cicd Pipelines Use Python CI/CD pipelines to remove setup chaos so Python projects stay predictable for every teammate.