Skip to content

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%"
simple: "$APP_HOST"
braced: "${APP_HOST}"
fallback_string: "${DATABASE_URL:-postgres://localhost:5432/dev}"
fallback_var: "${DATABASE_URL:-$FALLBACK_DB_URL}"
windows: "%APP_HOST%"
escape_dollar: "$$100"
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"
resolved_url: "${APP_HOST}/api/v1"
fallback_url: "${DATABASE_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"
default_set_url: "$KNOWN_HOST/api"
default_unset_url: "$UNSET_VAR/api"
empty_set_url: "$KNOWN_HOST/api"
empty_unset_url: "$UNSET_VAR/api"
disabled_set_url: "$KNOWN_HOST/api"
disabled_unset_url: "$UNSET_VAR/api"

In "strict" mode, all missing variables are collected and reported at once:

Config env expand errors (1)

  [host]  Missing environment variable 'MISSING_HOST'
   └── FILE 'config.yaml', line 1
       host: "$MISSING_HOST"
Config env expand errors (1)

  [host]  Missing environment variable 'MISSING_HOST'
   └── FILE 'config.json', line 1
       {"host": "$MISSING_HOST", "port": 8080}
Config env expand errors (1)

  [host]  Missing environment variable 'MISSING_HOST'
   └── FILE 'config.toml', line 1
       host = "$MISSING_HOST"
Config env expand errors (1)

  [host]  Missing environment variable 'MISSING_HOST'
   └── FILE 'config.ini', line 2
       host = $MISSING_HOST
Config env expand errors (1)

  [host]  Missing environment variable 'MISSING_HOST'
   └── ENV FILE 'config.env', line 1
       HOST=$MISSING_HOST

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.