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 condition built with the When() DSL. When("${TEMPLATE}") == "value" is true when the template expands to that value. when=None (the default) means always enabled.
import os
from dataclasses import dataclass
import dature
os.environ["APP_ENV"] = "dev"
@dataclass
class SecretsConfig:
vault_token: str = ""
cfg = dature.load(
dature.EnvSource(tag="secrets", when=dature.When("${APP_ENV}") == "prod"),
dature.EnvFileSource(
tag="secrets",
file=str(dev_env_path),
when=dature.When("${APP_ENV}").in_("dev", "local"),
),
schema=SecretsConfig,
)
assert cfg.vault_token == "dev-token-from-file"
Templates support the same ${VAR} and ${@tag.key} expansion syntax as source init-fields.
Combining conditions#
Use .in_() to enable the source when the template expands to any of several values. APP_ENV=local matches ("dev", "local"), so the source is enabled.
import os
from dataclasses import dataclass
import dature
os.environ["APP_ENV"] = "local"
@dataclass
class SecretsConfig:
vault_token: str = ""
cfg = dature.load(
dature.EnvFileSource(
tag="secrets",
file=str(dev_env_path),
when=dature.When("${APP_ENV}").in_("dev", "local"),
),
schema=SecretsConfig,
)
assert cfg.vault_token == "dev-token-from-file"
Use .not_in() to enable the source for every value except the listed ones. Here the file source loads in all environments except prod.
import os
from dataclasses import dataclass
import dature
os.environ["APP_ENV"] = "staging"
@dataclass
class SecretsConfig:
vault_token: str = ""
cfg = dature.load(
dature.EnvFileSource(
tag="secrets",
file=str(dev_env_path),
when=dature.When("${APP_ENV}").not_in("prod"), # disabled in prod only
),
schema=SecretsConfig,
)
assert cfg.vault_token == "dev-token-from-file"
Use & to require all conditions to match simultaneously. The source is enabled only when both APP_ENV=prod and REGION is eu or us.
import os
from dataclasses import dataclass
import dature
os.environ["APP_ENV"] = "prod"
os.environ["REGION"] = "eu"
@dataclass
class SecretsConfig:
vault_token: str = ""
cfg = dature.load(
dature.EnvFileSource(
tag="secrets",
file=str(dev_env_path),
when=(
(dature.When("${APP_ENV}") == "prod")
& dature.When("${REGION}").in_("eu", "us")
),
),
schema=SecretsConfig,
)
assert cfg.vault_token == "dev-token-from-file"
Use | to enable the source when any of the conditions matches. APP_ENV=staging satisfies the second branch, so the source is enabled.
import os
from dataclasses import dataclass
import dature
os.environ["APP_ENV"] = "staging"
@dataclass
class SecretsConfig:
vault_token: str = ""
cfg = dature.load(
dature.EnvFileSource(
tag="secrets",
file=str(dev_env_path),
when=(
(dature.When("${APP_ENV}") == "prod")
| (dature.When("${APP_ENV}") == "staging")
),
),
schema=SecretsConfig,
)
assert cfg.vault_token == "dev-token-from-file"
Use ~ to invert a condition. Here the source loads in every environment except prod.
import os
from dataclasses import dataclass
import dature
os.environ["APP_ENV"] = "dev"
@dataclass
class SecretsConfig:
vault_token: str = ""
cfg = dature.load(
dature.EnvFileSource(
tag="secrets",
file=str(dev_env_path),
when=~(dature.When("${APP_ENV}") == "prod"), # disabled in prod only
),
schema=SecretsConfig,
)
assert cfg.vault_token == "dev-token-from-file"
Conditions compose freely: (When("${A}") == "x") & (~When("${B}").in_("y", "z")).
Defaults for unset variables#
Use ${VAR:-default} when the variable may not be set. Both when= conditions must use the same default so they stay mutually exclusive:
import os
from dataclasses import dataclass
import dature
os.environ.pop("APP_ENV", None)
@dataclass
class SecretsConfig:
vault_token: str = ""
cfg = dature.load(
dature.EnvSource(
tag="secrets",
when=dature.When("${APP_ENV:-dev}") == "prod",
),
dature.EnvFileSource(
tag="secrets",
file=str(dev_env_path),
when=dature.When("${APP_ENV:-dev}").in_("dev", "local"),
),
schema=SecretsConfig,
)
assert cfg.vault_token == "dev-token-from-file"
Both conditions use the same default "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. For example, if APP_ENV is not set, ${APP_ENV} expands to an empty string and matches neither "prod" nor ("dev", "local"). If all sources are conditional and none matches, dature raises aDatureError immediately at construction time.
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=dature.When("${APP_ENV}") == "prod"),
dature.EnvFileSource(
tag="secrets",
file=dev_env_path,
when=dature.When("${APP_ENV}").in_("dev", "local"),
),
schema=SecretsConfig,
)
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:
import os
from dataclasses import dataclass
import dature
os.environ["APP_ENV"] = "prod"
os.environ["VAULT_TOKEN"] = "prod-token-from-env"
@dataclass
class SecretsConfig:
vault_token: str = ""
cfg = dature.load(
dature.EnvSource(tag="secrets", when=dature.When("${APP_ENV}") == "prod"),
dature.EnvFileSource(
tag="secrets",
file=str(dev_env_path),
when=dature.When("${APP_ENV}").in_("dev", "local"),
),
schema=SecretsConfig,
)
assert cfg.vault_token == "prod-token-from-env"
import os
from dataclasses import dataclass
import dature
os.environ["APP_ENV"] = "dev"
@dataclass
class SecretsConfig:
vault_token: str = ""
cfg = dature.load(
dature.EnvSource(tag="secrets", when=dature.When("${APP_ENV}") == "prod"),
dature.EnvFileSource(
tag="secrets",
file=str(dev_env_path),
when=dature.When("${APP_ENV}").in_("dev", "local"),
),
schema=SecretsConfig,
)
assert cfg.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() template when the toggle value lives in a file or another source rather than in an OS environment variable. For example, if the toggle value lives in config.json, not in an OS environment variable, JsonSource loads first, and its "env" key drives the when= condition of EnvFileSource.
from dataclasses import dataclass
import dature
@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=dature.When("${@cfg.env}").in_("dev", "local"),
),
schema=AppConfig,
)
assert cfg.vault_token == "dev-token-from-file"
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.
For example, config.json contains {"env": "dev"}. The "secrets" source is disabled lazily because its when= depends on ${@cfg.env}, and env != "prod". Even when disabled, sources still occupy their tag slot in the dependency graph, so ${@secrets.remote_config} remains a valid reference — it simply resolves to absent. In this case, the :- default is used and falls back to the local config.json instead.
from dataclasses import dataclass
import dature
@dataclass
class AppConfig:
env: str = ""
cfg = dature.load(
# {"env": "dev"}
dature.JsonSource(tag="cfg", file=str(config_path)),
# disabled: "dev" != "prod"
dature.EnvSource(tag="secrets", when=dature.When("${@cfg.env}") == "prod"),
# fallback
dature.JsonSource(file=f"${{@secrets.remote_config:-{config_path}}}"),
schema=AppConfig,
)
assert cfg.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. For example, base.env (e.g. DB_HOST, PORT) is always loaded, while the vault token is sourced from the OS environment in prod and from a local file in dev.
import os
from dataclasses import dataclass
import dature
os.environ["APP_ENV"] = "dev"
@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=dature.When("${APP_ENV}") == "prod"),
dature.EnvFileSource(
tag="secrets",
file=str(vault_dev_path),
when=dature.When("${APP_ENV}").in_("dev", "local"),
),
schema=AppConfig,
)
assert cfg.db_host == "db.internal"
assert cfg.port == 5432
assert cfg.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, and dature raises a DatureError at construction time. For example, if APP_ENV is not set, both when= conditions may fire simultaneously because they use different defaults, resulting in two active sources under the same explicit tag="secrets". Unlike a tag collision caused by ${@tag.key} references, this is detected at construction time whenever tag= is explicitly set - no consumer source is required. Fix: use a consistent default across all conditions (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=dature.When("${APP_ENV:-prod}") == "prod",
),
dature.EnvFileSource(
tag="secrets",
file="sources/vault_dev.env",
when=dature.When("${APP_ENV:-dev}").in_("dev", "local"),
),
schema=SecretsConfig,
)
dature.errors.exceptions.DatureError: Tag collision: multiple sources share resolved_tag='secrets':
EnvSource(expand_env_vars='default', tag='secrets', when=Match(template='${APP_ENV:-prod}', expected=('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:
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,
)
Fix: use the same :-default in both when= conditions so exactly one 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 / skip_if_missing=True (or the matching load-level flags) only apply to sources that pass the when= gate and then raise during loading. skip_if_broken covers parse/config errors; skip_if_missing covers absent files. A source disabled by its when= condition always takes priority over either flag.
Syntax reference#
| Condition | Section |
|---|---|
When("${APP_ENV}") == "prod" | Quick start |
When("${APP_ENV}") != "prod" | Combining conditions |
When("${APP_ENV}").in_("dev", "local") | Combining conditions |
When("${APP_ENV}").not_in("prod", "staging") | Combining conditions |
(When("${A}") == "x") & (When("${B}") == "y") — AND | Combining conditions |
(When("${A}") == "x") \| (When("${B}") == "y") — OR | Combining conditions |
~(When("${APP_ENV}") == "prod") — NOT | Combining conditions |
When("${APP_ENV:-dev}") == "prod" — default when unset | Defaults for unset variables |
When("${@tag.key}") == "prod" | Toggle from another source |