Skip to content

Conditional Sources#

Use when= to include a source only when a condition is met. A source that doesn't match is skipped entirely — it never touches the filesystem, the network, or the dependency graph.

Quick start#

Set when= to a mapping of template-string keys to expected values. The source is enabled if every key expands to the expected value.

"""Conditional sources — dev environment."""

import os
from dataclasses import dataclass
from pathlib import Path

import dature

os.environ["APP_ENV"] = "dev"

dev_env_path = Path(__file__).parent / "sources" / "vault_dev.env"


@dataclass
class SecretsConfig:
    vault_token: str = ""


cfg = dature.load(
    dature.EnvSource(tag="secrets", when={"${APP_ENV}": "prod"}),
    dature.EnvFileSource(
        tag="secrets",
        file=str(dev_env_path),
        when={"${APP_ENV}": ("dev", "local")},
    ),
    schema=SecretsConfig,
)

assert cfg.vault_token == "dev-token-from-file"
VAULT_TOKEN=dev-token-from-file

Keys support the same ${VAR} and ${@tag.key} expansion syntax as source init-fields. when=None or when={} (the default) means always enabled.

Allowing multiple values#

Pass a tuple to accept any of several values:

"""Conditional sources — multiple allowed values (tuple)."""

import os
from dataclasses import dataclass
from pathlib import Path

import dature

os.environ["APP_ENV"] = "local"

dev_env_path = Path(__file__).parent / "sources" / "vault_dev.env"


@dataclass
class SecretsConfig:
    vault_token: str = ""


cfg = dature.load(
    dature.EnvFileSource(
        tag="secrets",
        file=str(dev_env_path),
        when={"${APP_ENV}": ("dev", "local")},  # enabled for either value
    ),
    schema=SecretsConfig,
)

assert cfg.vault_token == "dev-token-from-file"

APP_ENV=local matches ("dev", "local"), so the source is enabled.

Combining conditions (AND)#

List multiple keys to require all of them to match simultaneously:

"""Conditional sources — multiple keys (AND semantics)."""

import os
from dataclasses import dataclass
from pathlib import Path

import dature

os.environ["APP_ENV"] = "prod"
os.environ["REGION"] = "eu"

dev_env_path = Path(__file__).parent / "sources" / "vault_dev.env"


@dataclass
class SecretsConfig:
    vault_token: str = ""


cfg = dature.load(
    dature.EnvFileSource(
        tag="secrets",
        file=str(dev_env_path),
        when={
            "${APP_ENV}": "prod",
            "${REGION}": ("eu", "us"),  # both keys must match
        },
    ),
    schema=SecretsConfig,
)

assert cfg.vault_token == "dev-token-from-file"

The source is enabled only when both APP_ENV=prod and REGION is eu or us. If either key doesn't match, the source is skipped.

Defaults for unset variables#

Use ${VAR:-default} when the variable may not be set. Both when= keys must share the same default so the conditions stay mutually exclusive:

"""Conditional sources — APP_ENV not set, using ${VAR:-default}."""

import os
from dataclasses import dataclass
from pathlib import Path

import dature

os.environ.pop("APP_ENV", None)

dev_env_path = Path(__file__).parent / "sources" / "vault_dev.env"


@dataclass
class SecretsConfig:
    vault_token: str = ""


cfg = dature.load(
    dature.EnvSource(tag="secrets", when={"${APP_ENV:-dev}": "prod"}),
    dature.EnvFileSource(
        tag="secrets",
        file=str(dev_env_path),
        when={"${APP_ENV:-dev}": ("dev", "local")},
    ),
    schema=SecretsConfig,
)

# APP_ENV unset → default "dev" matches → token from file
assert cfg.vault_token == "dev-token-from-file"
VAULT_TOKEN=dev-token-from-file

Both keys expand to "dev" when APP_ENV is unset — exactly one source is enabled, no collision.

Error: all sources filtered out#

Without a :-default, an unset variable expands to "", which matches nothing. If every source is conditional and none matches, dature raises immediately:

"""Conditional sources — error: all sources filtered out.

APP_ENV is not set, so ${APP_ENV} expands to "" which matches neither "prod"
nor ("dev", "local"). dature raises DatureError at construction time.
"""

import os
from dataclasses import dataclass
from pathlib import Path

import dature

os.environ.pop("APP_ENV", None)
dev_env_path = Path(__file__).parent / "sources" / "vault_dev.env"


@dataclass
class SecretsConfig:
    vault_token: str = ""


dature.load(
    dature.EnvSource(tag="secrets", when={"${APP_ENV}": "prod"}),
    dature.EnvFileSource(
        tag="secrets", file=dev_env_path, when={"${APP_ENV}": ("dev", "local")}
    ),
    schema=SecretsConfig,
)
  | dature.errors.exceptions.DatureConfigError: SecretsConfig loading errors (1)
  +-+---------------- 1 ----------------
    | dature.errors.exceptions.DatureError: Loader requires at least one enabled Source (all sources filtered out by when=)
    +------------------------------------

Switching environments#

The same pattern scales to prod. In prod the token is injected into the process environment by the deployment platform; in dev it comes from a local file. The dature.load() call is identical in both environments:

"""Conditional sources — prod environment."""

import os
from dataclasses import dataclass
from pathlib import Path

import dature

os.environ["APP_ENV"] = "prod"
os.environ["VAULT_TOKEN"] = (
    "prod-token-from-env"  # injected by the platform in real deployments
)

dev_env_path = Path(__file__).parent / "sources" / "vault_dev.env"


@dataclass
class SecretsConfig:
    vault_token: str = ""


cfg = dature.load(
    dature.EnvSource(tag="secrets", when={"${APP_ENV}": "prod"}),
    dature.EnvFileSource(
        tag="secrets",
        file=str(dev_env_path),
        when={"${APP_ENV}": ("dev", "local")},
    ),
    schema=SecretsConfig,
)

assert cfg.vault_token == "prod-token-from-env"
"""Conditional sources — dev environment."""

import os
from dataclasses import dataclass
from pathlib import Path

import dature

os.environ["APP_ENV"] = "dev"

dev_env_path = Path(__file__).parent / "sources" / "vault_dev.env"


@dataclass
class SecretsConfig:
    vault_token: str = ""


cfg = dature.load(
    dature.EnvSource(tag="secrets", when={"${APP_ENV}": "prod"}),
    dature.EnvFileSource(
        tag="secrets",
        file=str(dev_env_path),
        when={"${APP_ENV}": ("dev", "local")},
    ),
    schema=SecretsConfig,
)

assert cfg.vault_token == "dev-token-from-file"
VAULT_TOKEN=dev-token-from-file

Because when= conditions are mutually exclusive, only one source is ever active and both sources can safely share the same tag="secrets".

Toggle from another source#

Use ${@tag.key} as a when= key when the toggle value lives in a file or another source rather than in an OS environment variable:

"""Conditional sources — toggle from another source.

The toggle value lives in config.json, not in an OS env var.
JsonSource loads first; its "env" key drives the when= of EnvFileSource.
"""

from dataclasses import dataclass
from pathlib import Path

import dature

cfg_path = Path(__file__).parent / "sources" / "config.json"
vault_dev_path = Path(__file__).parent / "sources" / "vault_dev.env"


@dataclass
class AppConfig:
    vault_token: str = ""


cfg = dature.load(
    dature.JsonSource(tag="cfg", file=str(cfg_path)),
    dature.EnvFileSource(
        tag="secrets",
        file=str(vault_dev_path),
        when={"${@cfg.env}": ("dev", "local")},
    ),
    schema=AppConfig,
)

# cfg.env == "dev" → EnvFileSource enabled → token from file
assert cfg.vault_token == "dev-token-from-file"
{
  "env": "dev"
}

JsonSource loads first; its env key drives the when= of EnvFileSource. Unlike ${VAR} (resolved when load() is called), ${@tag.key} is resolved after the referenced source loads.

Referencing a disabled source#

A source disabled by a ${@tag.key}-based when= still occupies its tag slot in the dependency graph. Its data is empty, so a cross-ref to it without a default raises. Use :- to provide a fallback:

"""Conditional sources — referencing a disabled source with a fallback.

config.json contains {"env": "dev"}.  The "secrets" source is disabled lazily
(its when= depends on ${@cfg.env}) because env != "prod".

Disabled sources still occupy their tag slot in the dependency graph, so
${@secrets.remote_config} is a valid reference — it just resolves to absent.
The :- default kicks in and points to the local config.json instead.
"""

from dataclasses import dataclass
from pathlib import Path

import dature

config_path = Path(__file__).parent / "sources" / "config.json"


@dataclass
class AppConfig:
    env: str = ""


cfg = dature.load(
    dature.JsonSource(tag="cfg", file=str(config_path)),  # {"env": "dev"}
    dature.EnvSource(
        tag="secrets", when={"${@cfg.env}": "prod"}
    ),  # disabled: "dev" != "prod"
    dature.JsonSource(
        file=f"${{@secrets.remote_config:-{config_path}}}"
    ),  # fallback fires
    schema=AppConfig,
)

# secrets disabled → remote_config absent → fallback to config.json → env="dev"
assert cfg.env == "dev"
{
  "env": "dev"
}

secrets is disabled (cfg.env is "dev", not "prod"), so ${@secrets.remote_config} is absent — the :- default fires instead.

Same tag, different conditions#

when= enables or disables a Source instance as a whole. Multiple sources can share the same tag= as long as their conditions are mutually exclusive — at most one is active at a time. Use separate instances with different prefix= or field_mapping= to load different subsets of keys conditionally:

"""Conditional sources — same tag, different conditions.

when= enables or disables a Source instance as a whole.  To load some keys
unconditionally and others only in a specific environment, use separate Source
instances with different prefixes or field_mapping= targeting different subsets.

Here base.env (DB_HOST, PORT) is always loaded, while the vault token comes
from the OS environment in prod and from a local file in dev.
"""

import os
from dataclasses import dataclass
from pathlib import Path

import dature

os.environ["APP_ENV"] = "dev"

base_env_path = Path(__file__).parent / "sources" / "base.env"
vault_dev_path = Path(__file__).parent / "sources" / "vault_dev.env"


@dataclass
class AppConfig:
    db_host: str = ""
    port: int = 8080
    vault_token: str = ""


cfg = dature.load(
    dature.EnvFileSource(file=str(base_env_path)),  # always — DB_HOST, PORT
    dature.EnvSource(
        tag="secrets", when={"${APP_ENV}": "prod"}
    ),  # prod — VAULT_TOKEN from env
    dature.EnvFileSource(  # dev  — VAULT_TOKEN from file
        tag="secrets",
        file=str(vault_dev_path),
        when={"${APP_ENV}": ("dev", "local")},
    ),
    schema=AppConfig,
)

assert cfg.db_host == "db.internal"
assert cfg.port == 5432
assert cfg.vault_token == "dev-token-from-file"
DB_HOST=db.internal
PORT=5432
VAULT_TOKEN=dev-token-from-file

base.env is always loaded (no when=); only the secrets source switches.

Error: tag collision#

If conditions overlap, two sources with the same explicit tag= are both enabled — dature raises DatureError at construction time:

"""Conditional sources — error: tag collision (explicit tag=).

APP_ENV is not set. Both when= conditions fire simultaneously because they
use different defaults, leaving two sources enabled under the same explicit
tag="secrets".

Unlike a tag collision caused by ${@tag.key} references, dature detects
this at construction time whenever tag= is set explicitly — no consumer
source needed.

Fix: use the same default in both keys — see the no APP_ENV example.
"""

import os
from dataclasses import dataclass

import dature

os.environ.pop("APP_ENV", None)


@dataclass
class SecretsConfig:
    vault_token: str = ""


dature.load(
    dature.EnvSource(tag="secrets", when={"${APP_ENV:-prod}": "prod"}),
    dature.EnvFileSource(
        tag="secrets",
        file="sources/vault_dev.env",
        when={"${APP_ENV:-dev}": ("dev", "local")},
    ),
    schema=SecretsConfig,
)
dature.errors.exceptions.DatureError: Tag collision: multiple sources share resolved_tag='secrets':
  EnvSource(expand_env_vars='default', tag='secrets', when={'${APP_ENV:-prod}': 'prod'}, nested_resolve_strategy='flat')
  envfile 'sources/vault_dev.env'
Set an explicit tag= on at least one of them.

The same collision can appear with auto-tags (no explicit tag=): two sources of the same type share the same auto-tag, and the collision is detected only when a downstream source references that tag:

"""Conditional sources — error: tag collision (auto-tag, consumer reference).

Two EnvSources share the same auto-tag "env" (no tag= set explicitly).
Without a consumer, dature does not notice — but when VaultSource references
${@env.VAULT_TOKEN}, the ambiguous tag is detected and DatureError is raised.

Fix: assign an explicit tag= to at least one EnvSource.
"""

from dataclasses import dataclass

import dature


@dataclass
class AppConfig:
    vault_token: str = ""


dature.load(
    dature.EnvSource(),  # auto-tag "env"
    dature.EnvSource(prefix="BACKUP_"),  # auto-tag "env" — collision!
    dature.VaultSource(path="secret/app", token="${@env.VAULT_TOKEN}"),  # noqa: S106
    schema=AppConfig,
)
dature.errors.exceptions.DatureError: Tag collision: multiple sources share resolved_tag='env':
  EnvSource(expand_env_vars='default', nested_resolve_strategy='flat')
  EnvSource(prefix='BACKUP_', expand_env_vars='default', nested_resolve_strategy='flat')
Set an explicit tag= on at least one of them.

Fix: use the same :-default in both when= keys so exactly one condition matches when the variable is unset.

Interaction with skip_if_broken#

when= filtering runs before any I/O: a source that doesn't match its when= condition is never opened, never loaded, and never considered broken. skip_if_broken=True (or the skip_broken_sources=True load-level flag) only applies to sources that pass the when= gate and then raise during loading (e.g. file not found). In other words, when=False always takes priority over skip_if_broken.

Syntax reference#

Key form Section
"${APP_ENV}": "prod" Quick start
"${APP_ENV}": ("dev", "local") Multiple values
"${APP_ENV:-dev}": "prod", two keys with same default Defaults for unset variables
"${A}": …, "${B}": … — multiple keys Combining conditions
"${@tag.key}": "prod" Toggle from another source