Skip to content

Why Not Dynaconf?#

Dynaconf is a flexible configuration management library with multi-format support, layered environments, and dynamic reloading. It covers a lot of ground.

The trade-off is how it covers it: Dynaconf is powerful and battle-tested, but it trades type safety for flexibility — dynamic attribute access, no schema in code, and config result that's a dict-like object rather than your dataclass. dature takes a different approach: schema-first, type-safe, with your @dataclass as the single source of truth.

Side-by-Side#

Dynaconf dature
Config definition No schema — settings.FOO dynamic access stdlib @dataclass with type hints
Type safety Runtime casting, opt-in validation Enforced by type hints + automatic coercion
IDE support Weak — dynamic attributes, no autocompletion Full — typed dataclass fields
Static analysis (mypy) No Full support, including mypy plugin for decorator mode
Validation Separate Validator objects Both: Annotated inline validators + separate root/custom validators
Formats YAML, TOML, JSON, INI, .env, Python files YAML (1.1/1.2), JSON, JSON5, TOML (1.0/1.1), INI, .env, env vars, Docker secrets
Remote sources Vault, Redis + community plugins Not yet (planned)
Merging Layered override + dynaconf_merge 4 strategies + per-field rules ("append", "prepend", field groups, etc.)
Dynamic variables @format, @jinja templates with lazy evaluation ${VAR:-default} env expansion in all formats + file paths
CLI dynaconf list, inspect, write, validate, etc. dature inspect, dature validate (CLI)
Per-environment files Built-in ([development], [production] sections) Manual via multiple Source objects
Framework extensions Django, Flask built-in No — framework-agnostic by design
Feature flags Built-in simple system No
Error messages Generic exceptions Source file, line number, context snippet
Secret masking No built-in Auto-masks secrets in errors and logs
Debug / audit inspect_settings, get_history debug=True — which source provided each value
Config result Dynaconf object (dict-like) Your actual @dataclass instance

No Schema, No Safety Net#

Dynaconf doesn't require you to define what your configuration looks like:

# Dynaconf
from dynaconf import Dynaconf

settings = Dynaconf(settings_files=["config.toml"])

# Any attribute access "works" — even typos
print(settings.HOST)       # might be str, might be None
print(settings.HOSTT)      # no error, just returns empty
print(settings.PORT + 1)   # might crash at runtime if PORT is str

There's no schema in your code that says "these fields exist, with these types." Your IDE can't autocomplete, mypy can't check, and typos silently return empty values.

dature makes your config a typed dataclass:

@dataclass
class Config:
    host: str
    port: int
    debug: bool = False


config = dature.load(
    dature.Toml11Source(file=SOURCES_DIR / "dynaconf_basic.toml"),
    schema=Config,
)
# config.hostt → AttributeError immediately
# config.port is always int — guaranteed

Missing fields, wrong types, invalid values — all caught at load time with clear error messages pointing to the exact source file and line.

Validation: Separate vs. Both#

Dynaconf keeps validation separate from settings definition — and that's a valid approach for some teams:

# Dynaconf
from dynaconf import Dynaconf, Validator

settings = Dynaconf(
    settings_files=["config.toml"],
    validators=[
        Validator("PORT", gte=1, lte=65535),
        Validator("HOST", must_exist=True),
        Validator("DEBUG", is_type_of=bool),
    ],
)
settings.validators.validate()

This gives flexibility — validators can be defined in a different module, reused, or composed dynamically.

dature supports both approaches. Inline validators live with the type:

"""dature vs Dynaconf — inline Annotated validators."""

from dataclasses import dataclass
from pathlib import Path
from typing import Annotated

import dature
from dature import V

SOURCES_DIR = Path(__file__).parent / "sources"


@dataclass
class Config:
    host: str
    port: Annotated[int, (V > 0) & (V < 65536)]
    debug: bool = False


dature.load(
    dature.Toml11Source(file=SOURCES_DIR / "dynaconf_validators_invalid.toml"),
    schema=Config,
)
Error
  | dature.errors.exceptions.DatureConfigError: Config loading errors (1)
  +-+---------------- 1 ----------------
    | dature.errors.exceptions.FieldLoadError:   [port]  Value must be greater than 0
    |    ├── port = -1
    |    │          ^^
    |    └── FILE '{SOURCES_DIR}dynaconf_validators_invalid.toml', line 2
    +------------------------------------

And separate validators when you need cross-field checks or decoupled validation logic:

"""dature vs Dynaconf — root validators for cross-field checks."""

from dataclasses import dataclass
from pathlib import Path
from typing import Annotated

import dature
from dature import V

SOURCES_DIR = Path(__file__).parent / "sources"


@dataclass
class Config:
    host: str
    port: Annotated[int, (V > 0) & (V < 65536)]
    debug: bool = False


def check_debug_port(config: Config) -> bool:
    return not (config.debug and config.port == 80)


dature.load(
    dature.Toml11Source(
        file=SOURCES_DIR / "dynaconf_root_validators_invalid.toml",
        root_validators=(
            V.root(
                check_debug_port,
                error_message="debug mode should not use port 80",
            ),
        ),
    ),
    schema=Config,
)
Error
  | dature.errors.exceptions.DatureConfigError: Config loading errors (1)
  +-+---------------- 1 ----------------
    | dature.errors.exceptions.FieldLoadError:   [<root>]  debug mode should not use port 80
    |    └── FILE '{SOURCES_DIR}dynaconf_root_validators_invalid.toml'
    +------------------------------------

You choose the style that fits — or mix them.

Merging: Magic Keys vs. Explicit Strategies#

Dynaconf merges layers by overriding top-level keys. To merge nested structures instead of replacing them, you use special dynaconf_merge keys inside your config files:

# settings.toml
[databases]
host = "localhost"
port = 5432

# settings.local.toml — this REPLACES databases entirely
[databases]
port = 5433
# databases.host is now gone!

# To merge, you need:
[databases]
dynaconf_merge = true
port = 5433

This leaks infrastructure concerns into your config files. Every team member needs to know about dynaconf_merge, or they'll accidentally wipe nested sections.

dature uses explicit strategies in code:

config = dature.load(
    dature.Yaml12Source(file=SOURCES_DIR / "dynaconf_merge_defaults.yaml"),
    dature.Yaml12Source(
        file=SOURCES_DIR / "dynaconf_merge_local.yaml",
        skip_if_broken=True,
    ),
    schema=Config,
    strategy="last_wins",
)

No magic keys in config files. Merge behavior is defined in code, visible in one place.

Error Messages#

Dynaconf:

dynaconf.validator.ValidationError: PORT must gte 1 but it is 0 in env main

dature points to the exact value:

Config loading errors (1)

  [port]  Must be greater than 0
   ├── port = 0
   │          ^
   └── FILE 'config.toml', line 3

Source file, line number, the actual config line, and caret underline on the problematic value.

What Dynaconf Does Better#

To be fair — Dynaconf has mature features that dature doesn't (yet):

  • Remote sources — Vault, Redis integration out of the box. dature plans remote sources (Vault, AWS SSM) but doesn't have them yet.
  • CLI tooling — Dynaconf has list and write subcommands for runtime mutation; dature ships inspect and validate but does not have list/write.
  • Dynamic variables@format and @jinja templates with lazy evaluation and Python expressions. dature supports ${VAR:-default} env expansion in config values and file paths, but not Jinja templates.
  • Per-environment sections[development], [production] sections in a single file with automatic switching via ENV_FOR_DYNACONF.
  • Framework extensions — built-in Django and Flask integration.
  • Feature flags — simple built-in feature flag system.
  • Python files as config — load settings from .py files directly.

When to Use Dynaconf#

Dynaconf is a reasonable choice if:

  • You need remote config sources (Vault, Redis) today
  • You rely on CLI tooling for config inspection and management
  • You need per-environment sections in a single file
  • You want Jinja templates in config values
  • You use Django/Flask and want drop-in config integration
  • You prefer dynamic settings access without defining a schema

For type-safe, schema-first configuration — dature.