Skip to main content

YAML config loader with file includes, ${VAR} substitution, .env support, and deep merging

Project description

Loaden

YAML config loader with file includes, ${VAR} substitution, .env support, and deep merging.

# config.yaml
loaden_include: base.yaml       # compose from multiple files
loaden_env: .env                # load environment files

database:
  host: ${DB_HOST:-localhost}   # env var with default
  password: ${DB_PASSWORD}      # from .env file
from loaden import load_config, get

config = load_config("config.yaml")
host = get(config, "database.host", "127.0.0.1")  # safe nested access

Features

  • File includes - compose configs via loaden_include: [base.yaml, local.yaml]
  • Deep merging - nested dicts merge recursively, later values win
  • ${VAR} substitution - expand env vars with optional defaults ${VAR:-default}
  • .env file loading - load env files via loaden_env: .env
  • Safe nested access - get(config, "db.host", default) helper
  • Required key validation - fail fast on missing config
  • CLI tool - validate, show, combine, and extract configs

Installation

pip install loaden

Quick Start

from loaden import load_config

config = load_config("config.yaml")
print(config["database"]["host"])

Usage

Basic Configuration

Create a config.yaml:

database:
  host: localhost
  port: 5432
  name: myapp

logging:
  level: INFO

Load it:

from loaden import load_config

config = load_config("config.yaml")
# config = {"database": {"host": "localhost", "port": 5432, ...}, ...}

Include Files

Split configuration across multiple files using loaden_include:

base.yaml:

database:
  host: localhost
  port: 5432

logging:
  level: INFO

config.yaml:

loaden_include: base.yaml

database:
  name: production_db

api:
  key: secret123

Result after loading config.yaml:

{
    "database": {"host": "localhost", "port": 5432, "name": "production_db"},
    "logging": {"level": "INFO"},
    "api": {"key": "secret123"}
}

Multiple Includes

Include multiple files - they merge in order, with later files overriding earlier ones:

loaden_include:
  - defaults.yaml
  - database.yaml
  - local.yaml

app:
  name: myapp

Nested Includes

Included files can include other files:

common/logging.yaml:

logging:
  format: "%(levelname)s - %(message)s"

base.yaml:

loaden_include: common/logging.yaml

database:
  pool_size: 5

config.yaml:

loaden_include: base.yaml

database:
  host: prod.example.com

Environment Variables

Loaden provides three ways to work with environment variables:

1. Variable Substitution in Values

Use ${VAR} or ${VAR:-default} syntax in any string value:

database:
  host: ${DB_HOST:-localhost}
  password: ${DB_PASSWORD}
  url: postgres://${DB_HOST:-localhost}:5432/myapp
import os
os.environ["DB_HOST"] = "prod.example.com"

config = load_config("config.yaml")
print(config["database"]["host"])  # "prod.example.com"
print(config["database"]["url"])   # "postgres://prod.example.com:5432/myapp"

If a variable is not set and has no default, it remains as ${VAR} in the output.

2. Load Env Files with loaden_env

Load environment variables from .env or YAML files:

loaden_env: .env
# or multiple files:
loaden_env:
  - .env
  - secrets.env

database:
  password: ${DB_PASSWORD}

.env file format:

# Comments are ignored
DB_HOST=localhost
DB_PASSWORD="secret123"
API_KEY='my-api-key'

YAML env file format (secrets.yaml):

DB_PASSWORD: secret123
API_KEY: my-api-key

3. Set Env Vars with env Section

Set environment variables from config (useful for child processes):

env:
  DATABASE_URL: postgres://localhost/myapp
  API_TIMEOUT: 30
import os
from loaden import load_config

config = load_config("config.yaml")
print(os.environ["DATABASE_URL"])  # "postgres://localhost/myapp"

Shell environment always takes precedence - existing vars are not overwritten.

Required Keys Validation

Fail fast if required configuration is missing:

from loaden import load_config

config = load_config(
    "config.yaml",
    required_keys=["database.host", "database.port", "api.key"]
)

Raises ValueError with clear message if any key is missing:

ValueError: Invalid config: missing required keys in config.yaml: api.key

Safe Nested Access

Use get() to safely access nested keys without try/except:

from loaden import load_config, get

config = load_config("config.yaml")

# Safe access with default
host = get(config, "database.host", "localhost")
timeout = get(config, "api.timeout", 30)

# Returns None if not found (no default specified)
optional = get(config, "maybe.missing")

Deep Merge

Use deep_merge directly for custom merging:

from loaden import deep_merge

base = {"a": 1, "b": {"c": 2, "d": 3}}
overlay = {"b": {"d": 99, "e": 4}, "f": 5}

result = deep_merge(base, overlay)
# {"a": 1, "b": {"c": 2, "d": 99, "e": 4}, "f": 5}

CLI

Loaden includes a command-line tool for working with config files.

Validate

Check if a config file is valid:

loaden validate config.yaml
loaden validate config.yaml -v                    # verbose
loaden validate config.yaml -r "db.host,api.key"  # check required keys

Show

Display resolved config (with includes merged):

loaden show config.yaml           # full config
loaden show config.yaml -k database  # specific section

Combine

Merge multiple config files (later files override earlier):

loaden combine defaults.yaml local.yaml           # output to stdout
loaden combine defaults.yaml local.yaml -o out.yaml  # output to file

Extract

Extract a section to a new file:

loaden extract config.yaml database              # output to stdout
loaden extract config.yaml database -o db.yaml   # output to file

Merge Precedence

When using includes, values are merged with this precedence (highest wins):

  1. Main config file
  2. Later includes override earlier includes
  3. Included files (in order listed)

API Reference

load_config(config_path, required_keys=None, expand_vars=True)

Load configuration from a YAML file.

Parameters:

  • config_path (str): Path to the YAML config file
  • required_keys (list[str] | None): Dot-separated keys that must exist (e.g., ["db.host", "api.key"])
  • expand_vars (bool): Whether to expand ${VAR} in values (default: True)

Returns: dict[str, Any] - The configuration dictionary

Raises:

  • FileNotFoundError: Config file doesn't exist
  • yaml.YAMLError: Invalid YAML syntax
  • ValueError: Config is not a dict, circular include, or missing required keys

get(config, key_path, default=None)

Safely get a nested key using dot notation.

Parameters:

  • config (dict): Configuration dictionary
  • key_path (str): Dot-separated path (e.g., "database.host")
  • default (Any): Value to return if key not found (default: None)

Returns: Value at key path, or default if not found

deep_merge(base, overlay)

Recursively merge two dictionaries.

Parameters:

  • base (dict): Base dictionary
  • overlay (dict): Dictionary to merge on top (takes precedence)

Returns: dict - New merged dictionary (inputs not modified)

Development

# Create virtual environment
python3 -m venv ~/Environments/loaden
source ~/Environments/loaden/bin/activate

# Install with dev dependencies
pip install -e ".[dev]"

# Run tests
pytest tests/ -v

# Lint and format
ruff check .
ruff format .

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

loaden-0.1.0.tar.gz (28.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

loaden-0.1.0-py3-none-any.whl (21.5 kB view details)

Uploaded Python 3

File details

Details for the file loaden-0.1.0.tar.gz.

File metadata

  • Download URL: loaden-0.1.0.tar.gz
  • Upload date:
  • Size: 28.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.2

File hashes

Hashes for loaden-0.1.0.tar.gz
Algorithm Hash digest
SHA256 a2dde489b531fa15457e9ea183b96c77975c655b83ad5fe264a5f4c737a923f5
MD5 f101355addbd93cdcbfd61ac9efd3ed1
BLAKE2b-256 75c3442d84913686694e7c69e877180b7e2e4b0100f416574715404fb445ca9c

See more details on using hashes here.

File details

Details for the file loaden-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: loaden-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 21.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.2

File hashes

Hashes for loaden-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 170e7f1256832c03fcd7e88c832686727be59b056fbf11c8e3ba8f800020ef67
MD5 1562cd18e201343d8afb8887f0ef6fc1
BLAKE2b-256 675a5fdd6662d0fb5a1cae8e1b3a0a6d169324cb92825eca31d61eb2ca0cfba6

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page