Spring Boot-style configuration for Python applications
Understanding how SprigConfig merges configuration files is essential for predictable behavior. This guide explains the merge order, deep merge semantics, and how to debug merge issues.
SprigConfig loads and merges configuration in this exact order:
1. application.<ext> ← Base configuration
2. Base imports ← Files from base's imports: directive
3. application-<profile>.<ext> ← Profile overlay
4. Profile imports ← Files from profile's imports: directive
Each step merges into the result of the previous step.
Given these files:
# application.yml
server:
port: 8080
host: localhost
imports:
- defaults.yml
# defaults.yml
server:
timeout: 30
# application-dev.yml
server:
port: 9090
imports:
- dev-extras.yml
# dev-extras.yml
server:
debug: true
The merge happens as:
application.yml → {server: {port: 8080, host: localhost}}defaults.yml → {server: {port: 8080, host: localhost, timeout: 30}}application-dev.yml → {server: {port: 9090, host: localhost, timeout: 30}}dev-extras.yml → {server: {port: 9090, host: localhost, timeout: 30, debug: true}}Key insight: Profile overlays have the final say. Values in application-dev.yml override imported values from defaults.yml.
SprigConfig uses a recursive deep merge algorithm with these rules:
Dictionaries are merged recursively. Keys from both are preserved, with the overlay taking precedence for conflicts.
# base
server:
host: localhost
port: 8080
# overlay
server:
port: 9090
timeout: 30
# result
server:
host: localhost # from base
port: 9090 # from overlay (overrides)
timeout: 30 # from overlay (new)
Lists are completely replaced, not appended.
# base
features:
- auth
- logging
# overlay
features:
- caching
# result
features:
- caching # overlay replaces entire list
If you want to extend a list, you must repeat all items in the overlay.
Scalar values (strings, numbers, booleans) are replaced.
# base
port: 8080
# overlay
port: 9090
# result
port: 9090
Keys present in the base but missing in the overlay are preserved.
# base
server:
host: localhost
port: 8080
# overlay
server:
port: 9090
# host is not mentioned
# result
server:
host: localhost # preserved from base
port: 9090 # from overlay
SprigConfig warns when overlays might unintentionally lose keys. These warnings help catch configuration mistakes.
When an overlay provides only some keys from a nested structure:
# base
database:
host: localhost
port: 5432
username: admin
password: secret
# overlay
database:
host: db.prod.com
# port, username, password not mentioned
SprigConfig logs a warning indicating that database is being partially overridden. The other keys are preserved, but the warning helps catch cases where you might have forgotten to include them.
If your partial override is intentional, you can suppress warnings:
# application.yml
suppress_config_merge_warnings: true
Or per-section in your code when using deep_merge directly:
from sprigconfig import deep_merge
result = deep_merge(base, overlay, suppress=True)
Imports are processed depth-first as they’re encountered. Within an imports list, files are processed in order.
# application.yml
imports:
- a.yml
- b.yml
Processing order:
application.ymla.yml and mergea.yml has imports, process them recursivelyb.yml and mergeb.yml has imports, process them recursivelyImports can appear at any level in the configuration tree:
# application.yml
server:
imports:
- server-defaults.yml
database:
imports:
- database-defaults.yml
When imports appear under a key (like server), the imported content merges at that level, not at the root.
# server-defaults.yml
port: 8080
host: localhost
# Results in:
server:
port: 8080
host: localhost
The easiest way to see the final merged result:
sprigconfig dump --config-dir=config --profile=dev
The merged config includes information about what was loaded:
cfg = load_config(profile="dev")
# See all source files
for source in cfg["sprigconfig._meta.sources"]:
print(f"Loaded: {source}")
# See the import trace
import_trace = cfg["sprigconfig._meta.import_trace"]
SprigConfig logs merge operations. Enable debug logging to see details:
import logging
logging.basicConfig(level=logging.DEBUG)
You’ll see messages like:
Use a defaults.yml file imported by the base configuration:
# application.yml
imports:
- defaults.yml
# Only specify what's different from defaults
server:
host: myapp.example.com
Keep database credentials in profile files:
# application.yml
database:
driver: postgresql
pool_size: 5
# application-prod.yml
database:
host: ${DB_HOST}
password: ENC(...)
pool_size: 20
# application.yml
features:
new_ui: false
beta_api: false
# application-dev.yml
features:
new_ui: true
beta_api: true
# application-prod.yml
features:
new_ui: true # Rolled out to production
beta_api: false # Not yet in production