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

Featurepython-dotenvDynaconfHydra
Multi-format support.env onlyTOML, YAML, JSON, INI, .envYAML only
Environment switchingManual file selectionBuilt-in sectionsConfig groups
ValidationNoneBuilt-in validatorsVia structured configs
Secrets managementNoneVault, Redis built-inManual
Live reloadingNoYesNo
CLI overridesNoLimitedFull command-line
CompositionNoLayered mergingHierarchical groups
Best forSimple projectsWeb apps, servicesML 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.

pythondynaconfconfigurationsettingsvault

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.