Spring Boot-style configuration for Python applications
This guide covers the core concepts and API for working with SprigConfig.
When you load configuration, SprigConfig returns a Config object—an immutable-ish mapping wrapper that provides convenient access patterns.
from sprigconfig import load_config
cfg = load_config(profile="dev")
Config objects support standard dictionary operations:
# Direct key access
port = cfg["server"]["port"]
# Iteration
for key in cfg:
print(key, cfg[key])
# Length
print(len(cfg))
# Key membership
if "server" in cfg:
print("Server config found")
For deeply nested values, use dotted-key notation:
# These are equivalent
port = cfg["server"]["port"]
port = cfg["server.port"]
# With default value
port = cfg.get("server.port", 8080)
# Check existence
if "server.port" in cfg:
print("Port configured")
When you access a nested dictionary, it’s automatically wrapped as a Config object:
server = cfg["server"] # Returns a Config object
print(server["port"]) # 8080
print(server.get("host", "localhost"))
from sprigconfig import load_config
# Load with profile
cfg = load_config(profile="dev")
# Load with custom directory
cfg = load_config(profile="prod", config_dir=Path("/opt/myapp/config"))
For more control, use ConfigLoader directly:
from pathlib import Path
from sprigconfig import ConfigLoader
loader = ConfigLoader(
config_dir=Path("config"),
profile="dev",
ext="yml" # or "json", "toml"
)
cfg = loader.load()
| Parameter | Type | Description |
|---|---|---|
config_dir |
Path |
Directory containing configuration files |
profile |
str |
Active profile (dev, test, prod, etc.) |
ext |
str |
File extension (yml, yaml, json, toml) |
SprigConfig supports three configuration formats:
# application.yml
server:
port: 8080
host: localhost
{
"server": {
"port": 8080,
"host": "localhost"
}
}
[server]
port = 8080
host = "localhost"
The format is determined by:
ConfigLoader(ext="json")SPRIGCONFIG_FORMAT=jsonymlImportant: All files in a single load must use the same format. You cannot mix YAML and JSON files.
Configuration values can reference environment variables using ${VAR} syntax:
database:
host: ${DB_HOST}
port: ${DB_PORT:5432}
username: ${DB_USER:admin}
| Pattern | Behavior |
|---|---|
${VAR} |
Expands to value of VAR, or remains unexpanded if missing |
${VAR:default} |
Expands to value of VAR, or uses default if missing |
Environment variables are expanded at load time, before YAML/JSON/TOML parsing.
export DB_HOST=db.example.com
cfg = load_config(profile="prod")
print(cfg["database.host"]) # db.example.com
print(cfg["database.port"]) # 5432 (default used)
Every loaded configuration includes metadata under sprigconfig._meta:
cfg = load_config(profile="dev")
meta = cfg["sprigconfig"]["_meta"]
print(meta["profile"]) # dev
print(meta["sources"]) # List of loaded files
print(meta["import_trace"]) # Import graph details
| Field | Description |
|---|---|
profile |
The active profile |
sources |
Ordered list of file paths that were loaded |
import_trace |
Detailed import graph with depth and order |
# See all files that contributed to this config
for source in cfg["sprigconfig._meta.sources"]:
print(f"Loaded: {source}")
# Check which profile is active
if cfg["sprigconfig._meta.profile"] == "prod":
print("Running in production mode")
# Get plain dict (secrets redacted)
data = cfg.to_dict()
# Get plain dict with secrets revealed (unsafe!)
data = cfg.to_dict(reveal_secrets=True)
# Dump to YAML string (secrets redacted)
yaml_str = cfg.dump()
# Dump to JSON string
json_str = cfg.dump(output_format="json")
# Write to file
cfg.dump(Path("config-snapshot.yml"))
# Dump with secrets (unsafe!)
cfg.dump(safe=False)
For applications that need global access to configuration, use ConfigSingleton:
from sprigconfig import ConfigSingleton
# At application startup (once only)
ConfigSingleton.initialize(
profile="prod",
config_dir="config"
)
# From anywhere in your application
def get_database_config():
cfg = ConfigSingleton.get()
return {
"host": cfg["database.host"],
"port": cfg["database.port"],
}
ConfigSingleton is thread-safe. Multiple threads can call get() safely.
initialize() must be called exactly onceinitialize() twice raises an errorget() before initialize() raises an error_clear_all() only in testsSprigConfig raises ConfigLoadError for configuration problems:
from sprigconfig import load_config, ConfigLoadError
try:
cfg = load_config(profile="prod")
except ConfigLoadError as e:
print(f"Configuration error: {e}")
| Error | Cause |
|---|---|
| Missing file | Required file (e.g., application-prod.yml) not found |
| Invalid YAML/JSON/TOML | Syntax error in configuration file |
| Circular import | Import chain creates a cycle |
| Missing secret key | APP_SECRET_KEY not set for encrypted values |
| Invalid secret key | Key is not a valid Fernet key |
SprigConfig automatically handles UTF-8 BOM (Byte Order Mark) in configuration files. Files created on Windows with BOM markers are read correctly without producing malformed keys.
This is handled transparently—you don’t need to do anything special.
Your application.yml should contain all keys with sensible defaults. Profile overlays should only override what’s different.
# Prefer this
port = cfg.get("server.database.connection.port", 5432)
# Over this
port = cfg.get("server", {}).get("database", {}).get("connection", {}).get("port", 5432)
def test_production_config():
cfg = load_config(profile="prod")
assert cfg["sprigconfig._meta.profile"] == "prod"
assert "application-prod.yml" in str(cfg["sprigconfig._meta.sources"])
Load configuration once at startup and pass values to components, rather than passing the Config object around.