ENV Variable Expansion#
String values in all file formats support environment variable expansion:
"""ENV variable expansion — all supported syntax variants."""
import os
from dataclasses import dataclass
from pathlib import Path
import dature
SOURCES_DIR = Path(__file__).parent / "sources"
os.environ["APP_HOST"] = "https://api.example.com"
os.environ["FALLBACK_DB_URL"] = "postgres://fallback:5432/db"
@dataclass
class Config:
simple: str
braced: str
fallback_string: str
fallback_var: str
windows: str
escape_dollar: str
escape_percent: str
config = dature.load(
dature.Yaml12Source(
file=SOURCES_DIR / "advanced_env_expansion.yaml",
expand_env_vars="default",
),
schema=Config,
)
assert config.simple == "https://api.example.com"
assert config.braced == "https://api.example.com"
assert config.fallback_string == "postgres://localhost:5432/dev"
assert config.fallback_var == "postgres://fallback:5432/db"
assert config.windows == "https://api.example.com"
assert config.escape_dollar == "$100"
assert config.escape_percent == "100%"
Supported Syntax#
| Syntax | Description |
|---|---|
$VAR | Subsitute variable |
${VAR} | Substitute variable (alterative form) |
${VAR:-default} | Variable with fallback value |
${VAR:-$FALLBACK_VAR} | Fallback is also an env variable |
%VAR% | Substitute variable (alterative windows-like form) |
$$ | Literal $ (escaped) |
%% | Literal % (escaped) |
Expansion Modes#
| Mode | Missing variable |
|---|---|
"default" | Kept as-is ($VAR stays $VAR) |
"empty" | Replaced with "" |
"strict" | Raises EnvVarExpandError |
"disabled" | No expansion at all |
The "default" mode is named so because it matches the behavior of Python's built-in os.path.expandvars() — missing variables are kept as-is rather than being replaced with empty strings or raising errors.
Set the mode on Source:
"""ENV expansion — strict mode on Source."""
import os
from dataclasses import dataclass
from pathlib import Path
import dature
SOURCES_DIR = Path(__file__).parent / "sources"
os.environ["APP_HOST"] = "https://api.example.com"
@dataclass
class Config:
resolved_url: str
fallback_url: str
config = dature.load(
dature.Yaml12Source(
file=SOURCES_DIR / "advanced_env_expansion_strict.yaml",
expand_env_vars="strict",
),
schema=Config,
)
assert config.resolved_url == "https://api.example.com/api/v1"
assert config.fallback_url == "postgres://localhost:5432/dev"
For merge mode, pass expand_env_vars to dature.load() as default for all sources:
"""ENV expansion — merge mode with per-source override."""
import os
from dataclasses import dataclass
from pathlib import Path
import dature
SOURCES_DIR = Path(__file__).parent / "sources"
os.environ["KNOWN_HOST"] = "https://api.example.com"
@dataclass
class Config:
default_set_url: str
default_unset_url: str
empty_set_url: str
empty_unset_url: str
disabled_set_url: str
disabled_unset_url: str
config = dature.load(
dature.Yaml12Source(
file=SOURCES_DIR / "advanced_env_expansion_merge_default.yaml",
), # uses global "default"
dature.Yaml12Source(
file=SOURCES_DIR / "advanced_env_expansion_merge_empty.yaml",
expand_env_vars="empty",
),
dature.Yaml12Source(
file=SOURCES_DIR / "advanced_env_expansion_merge_disabled.yaml",
expand_env_vars="disabled",
),
schema=Config,
expand_env_vars="default", # global default for all sources
)
assert config.default_set_url == "https://api.example.com/api"
assert config.default_unset_url == "$UNSET_VAR/api"
assert config.empty_set_url == "https://api.example.com/api"
assert config.empty_unset_url == "/api"
assert config.disabled_set_url == "$KNOWN_HOST/api"
assert config.disabled_unset_url == "$UNSET_VAR/api"
In "strict" mode, all missing variables are collected and reported at once:
The ${VAR:-default} fallback syntax works in all modes.
File Path Expansion#
Environment variables in the file=... parameter of Source subclasses are expanded automatically in "strict" mode — if a variable is missing, EnvVarExpandError is raised immediately at Source creation time.
This works for both directory paths and file names:
"""ENV expansion — variable in directory path."""
import os
from dataclasses import dataclass
from pathlib import Path
import dature
SOURCES_DIR = Path(__file__).parent / "sources"
os.environ["DATURE_SOURCES_DIR"] = str(SOURCES_DIR)
@dataclass
class Config:
host: str
port: int
config = dature.load(
dature.Yaml12Source(
file="$DATURE_SOURCES_DIR/advanced_env_expansion_file_path.yaml",
),
schema=Config,
)
assert config.host == "localhost"
assert config.port == 8080
"""ENV expansion — variable in file name."""
import os
from dataclasses import dataclass
from pathlib import Path
import dature
SOURCES_DIR = Path(__file__).parent / "sources"
os.environ["DATURE_APP_ENV"] = "production"
@dataclass
class Config:
host: str
port: int
config = dature.load(
dature.Yaml12Source(file=str(SOURCES_DIR / "config.$DATURE_APP_ENV.yaml")),
schema=Config,
)
assert config.host == "prod.example.com"
assert config.port == 443
"""ENV expansion — variables in both directory path and file name."""
import os
from dataclasses import dataclass
from pathlib import Path
import dature
SOURCES_DIR = Path(__file__).parent / "sources"
os.environ["DATURE_SOURCES_DIR"] = str(SOURCES_DIR)
os.environ["DATURE_APP_ENV"] = "production"
@dataclass
class Config:
host: str
port: int
config = dature.load(
dature.Yaml12Source(file="$DATURE_SOURCES_DIR/config.$DATURE_APP_ENV.yaml"),
schema=Config,
)
assert config.host == "prod.example.com"
assert config.port == 443
All supported syntax ($VAR, ${VAR}, ${VAR:-default}, %VAR%) works in file paths.
str and Path values are both expanded. File-like objects and None are passed through unchanged.
Note
File path expansion is always "strict", independent of the expand_env_vars setting. The expand_env_vars parameter controls expansion of values inside config files, while file paths are expanded at Source creation time. A missing variable in a file path would lead to a confusing FileNotFoundError, so strict validation is enforced.