Varlord
Unified yet dead simple Python config management: defaults, CLI, env vars, .env, etcd. Type-safe, validated, with dynamic updates.
Install / Use
/learn @lzjever/VarlordREADME
Varlord ⚙️
Stop wrestling with configuration chaos. Start with Varlord.
Varlord is a battle-tested Python configuration management library that eliminates the pain of managing configuration from multiple sources. Born from real-world production challenges, it provides a unified, type-safe, and elegant solution for configuration management.
🎯 The Problem We Solve
Real-World Configuration Nightmares
Every Python developer has faced these frustrating scenarios:
❌ The Configuration Spaghetti
# Your code becomes a mess of conditionals and parsing
host = os.getenv("HOST", "127.0.0.1")
port = int(os.getenv("PORT", "8000")) # What if PORT is not a number?
debug = os.getenv("DEBUG", "false").lower() == "true" # Really?
if "--host" in sys.argv:
host = sys.argv[sys.argv.index("--host") + 1] # Error-prone parsing
# ... and it gets worse with nested configs, validation, etc.
❌ Priority Confusion
"Does CLI override env? Or env overrides CLI? Wait, what about the config file? Which one wins?"
❌ Type Conversion Hell
# String "true" vs boolean True vs "1" vs 1
# "8000" vs 8000
# Missing values, None handling, type errors at runtime...
❌ The Restart Tax
"I just need to change one config value. Why do I have to restart the entire service?"
❌ Silent Failures
"The config looks wrong, but the app starts anyway. Users report bugs 3 hours later."
✅ Varlord's Solution
One unified interface. Multiple sources. Clear priority. Built-in diagnostics.
from dataclasses import dataclass, field
from pathlib import Path
from typing import Optional
from varlord import Config, sources
@dataclass(frozen=True)
class AppConfig:
"""Application configuration with clear structure and validation."""
# Required field - must be provided
api_key: str = field(metadata={"description": "API key for authentication"})
# Optional fields with sensible defaults
host: str = field(default="127.0.0.1", metadata={"description": "Server host address"})
port: int = field(default=8000, metadata={"description": "Server port number"})
debug: bool = field(default=False, metadata={"description": "Enable debug mode"})
timeout: float = field(default=30.0, metadata={"description": "Request timeout in seconds"})
hello_message: Optional[str] = field(
default=None, metadata={"description": "Optional greeting message"}
)
def main():
# Define configuration sources with clear priority order
# Priority: CLI (highest) > User Config > App Config > System Config > Env > Defaults (lowest)
cfg = Config(
model=AppConfig,
sources=[
# System-wide configuration (lowest priority, rarely overridden)
sources.YAML("/etc/myapp/config.yaml"), # System config
# Application-level configuration
sources.JSON(Path(__file__).parent / "config.json"), # App directory
# User-specific configuration (overrides system and app configs)
sources.YAML(Path.home() / ".config" / "myapp" / "config.yaml"), # User directory
sources.TOML(Path.home() / ".myapp.toml"), # Alternative user config
# Environment variables (common in containers/CI)
sources.Env(),
sources.DotEnv(".env"), # Local development
# Command-line arguments (highest priority, for debugging/overrides)
sources.CLI(),
],
)
# One line to add comprehensive CLI management: --help, --check-variables, etc.
# This single call adds:
# - --help / -h: Auto-generated help from your model metadata
# - --check-variables / -cv: Complete configuration diagnostics
# - Automatic validation and error reporting
# - Exit handling (exits if help/cv is requested)
cfg.handle_cli_commands() # Handles --help, -cv automatically, exits if needed
# Load configuration - type-safe, validated, ready to use
app = cfg.load()
# Your application code
print(f"Starting server on {app.host}:{app.port}")
print(f"Debug: {app.debug}, Timeout: {app.timeout}s")
if __name__ == "__main__":
main()
What just happened?
- ✅ Multiple Sources, Unified Interface: System config, app config, user config, env vars, CLI - all handled the same way
- ✅ Clear Priority: Later sources override earlier ones - no confusion
- ✅ Automatic Type Conversion: Strings from files/env → proper types (int, bool, float)
- ✅ Model-Driven Filtering: Each source only reads fields defined in your model
- ✅ Built-in Diagnostics:
--check-variablesshows exactly what's loaded from where - ✅ Zero Boilerplate: No parsing, no type conversion code, no priority logic
Try it:
# See comprehensive configuration diagnostics
python app.py --check-variables
# or short form
python app.py -cv
# See help with all sources and priority
python app.py --help
# Run normally
python app.py --api-key your_key
The --check-variables output shows everything:
When you run python app.py -cv, you get a comprehensive diagnostic report:
+---------------+----------+---------------+----------+-----------+
| Variable | Required | Status | Source | Value |
+---------------+----------+---------------+----------+-----------+
| api_key | Required | Missing | defaults | None |
| host | Optional | Loaded | dotenv | localhost |
| port | Optional | Loaded | dotenv | 7000 |
| debug | Optional | Loaded | dotenv | true |
| timeout | Optional | Loaded | dotenv | 20.0 |
| hello_message | Optional | Using Default | defaults | None |
+---------------+----------+---------------+----------+-----------+
Configuration Source Priority and Details:
+------------+-------------+-----------+----------------------------------------+--------+----------------+---------------+-------------+
| Priority | Source Name | Source ID | Instance | Status | Load Time (ms) | Watch Support | Last Update |
+------------+-------------+-----------+----------------------------------------+--------+----------------+---------------+-------------+
| 1 (lowest) | defaults | defaults | <Defaults(model=AppConfig)> | Active | 0.00 | No | N/A |
| 2 | yaml | yaml | <YAML(/etc/myapp/config.yaml)> | Active | 0.15 | No | N/A |
| 3 | json | json | <JSON(config.json)> | Active | 0.08 | No | N/A |
| 4 | yaml | yaml | <YAML(~/.config/myapp/config.yaml)> | Active | 0.12 | No | N/A |
| 5 | toml | toml | <TOML(~/.myapp.toml)> | Active | 0.05 | No | N/A |
| 6 | env | env | <Env(model-based)> | Active | 0.05 | No | N/A |
| 7 | dotenv | dotenv | <DotEnv(.env)> | Active | 0.03 | No | N/A |
| 8 (highest)| cli | cli | <CLI()> | Active | 0.20 | No | N/A |
+------------+-------------+-----------+----------------------------------------+--------+----------------+---------------+-------------+
Note: Later sources override earlier ones (higher priority).
⚠️ Missing required fields: api_key
Exiting with code 1. Please provide these fields and try again.
For help, run: python app.py --help
What this tells you:
- Variable Status: See which fields are required vs optional, loaded vs missing
- Source Tracking: Know exactly which source (defaults/env/cli/file) provided each value
- Priority Order: Understand the resolution chain - later sources override earlier
- Performance: Load times for each source (useful for optimization)
- Validation: Missing required fields are caught immediately with clear error messages
Key Benefits:
- 🔍 Complete Visibility: See exactly which source provides each value - no more guessing where config comes from
- 📊 Priority Visualization: Understand the resolution order at a glance - see which source wins for each field
- ⚡ Performance Metrics: Load times for each source - identify slow config sources
- 🛡️ Validation: Missing required fields are caught before app starts - fail fast with clear errors
- 📝 Self-Documenting: Help text generated from your model metadata - no manual documentation needed
- 🎯 Zero Configuration:
handle_cli_commands()adds all this with one line - no boilerplate
Real-World Scenarios:
- Debugging: "Why is my app using the wrong port?" →
python app.py -cvshows port comes from env, not CLI - Onboarding: New team member runs
python app.py --help→ sees all config options with descriptions - CI/CD: Missing required field? →
-cvshows exactly what's missing before deployment fails - Multi-Environment: See which config file (system/user/app)
