A powerful Python configuration management library with support for defaults, CLI args, environment variables, .env files, and optional etcd integration with dynamic updates
Project description
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) is actually being used
That's it. No parsing, no type conversion, no priority confusion. Just clean, type-safe configuration with built-in diagnostics.
๐ Why Varlord?
๐ฏ Core Value Propositions
| Problem | Varlord Solution | Impact |
|---|---|---|
| Config scattered everywhere | Unified interface for all sources | Single source of truth |
| Priority confusion | Simple rule: later sources override earlier | Predictable behavior |
| Type conversion errors | Automatic conversion with validation | Catch errors early |
| No runtime updates | Optional etcd watch for dynamic updates | Zero-downtime config changes |
| Repetitive boilerplate | Model-driven, auto-filtering | 90% less code |
| Silent failures | Built-in validation framework | Fail fast, fail clear |
๐ก Key Differentiators
- ๐ฏ Model-Driven Design: Define your config once as a dataclass, and Varlord handles the rest
- ๐ Smart Auto-Filtering: Sources automatically filter by model fields - no prefix management needed
- โก Zero Boilerplate: Model defaults are automatic, model is auto-injected to sources
- ๐ก๏ธ Type Safety First: Full type hints support with automatic conversion and validation
- ๐ Production Ready: Thread-safe, fail-safe, battle-tested in production environments
๐ Quick Start
Installation
pip install varlord
# With optional features
pip install varlord[dotenv,etcd]
Basic Usage (30 seconds)
from dataclasses import dataclass, field
from varlord import Config, sources
@dataclass(frozen=True)
class AppConfig:
host: str = field(default="127.0.0.1")
port: int = field(default=8000)
debug: bool = field(default=False)
# Create config - that's it!
cfg = Config(
model=AppConfig,
sources=[
sources.Env(), # Reads HOST, PORT, DEBUG from environment
sources.CLI(), # Reads --host, --port, --debug from CLI
],
)
app = cfg.load() # Type-safe, validated config object
print(f"Server: {app.host}:{app.port}, Debug: {app.debug}")
Run it:
# Use defaults
python app.py
# Output: Server: 127.0.0.1:8000, Debug: False
# Override with env
export HOST=0.0.0.0 PORT=9000
python app.py
# Output: Server: 0.0.0.0:9000, Debug: False
# Override with CLI (highest priority)
python app.py --host 192.168.1.1 --port 8080 --debug
# Output: Server: 192.168.1.1:8080, Debug: True
One-Liner Convenience Method
# Even simpler for common cases
cfg = Config.from_model(AppConfig, cli=True, dotenv=".env")
app = cfg.load()
๐ผ Real-World Use Cases
Use Case 1: Microservice Configuration
Problem: Your microservice needs config from multiple sources, and you're tired of writing parsing code.
Solution:
@dataclass(frozen=True)
class ServiceConfig:
db_host: str = field(default="localhost")
db_port: int = field(default=5432)
api_key: str = field() # Required - must be provided
log_level: str = field(default="INFO")
max_workers: int = field(default=4)
cfg = Config(
model=ServiceConfig,
sources=[
sources.Env(), # Production: from environment
sources.DotEnv(".env"), # Development: from .env file
sources.CLI(), # Override: from command line
],
)
config = cfg.load() # Validated, type-safe, ready to use
Benefits:
- โ
Same code works in dev (
.env), staging (env vars), and prod (env vars) - โ
CLI overrides for debugging:
python service.py --log-level DEBUG - โ
Type safety:
max_workersis always anint, never a string - โ
Validation: Missing
api_keyfails fast with clear error
Use Case 2: Dynamic Configuration Updates
Problem: You need to change configuration without restarting the service.
Solution:
def on_config_change(new_config, diff):
print(f"Config updated: {diff}")
# Update your app's behavior based on new config
cfg = Config(
model=AppConfig,
sources=[
sources.Env(),
sources.Etcd(
host="etcd.example.com",
prefix="/app/config/",
watch=True, # Enable dynamic updates
),
],
)
store = cfg.load_store() # Returns ConfigStore for dynamic updates
store.subscribe(on_config_change)
# Thread-safe access to current config
current = store.get()
Benefits:
- โ Zero-downtime configuration updates
- โ Thread-safe concurrent access
- โ Automatic validation on updates
- โ Change notifications via callbacks
Use Case 3: Multi-Environment Deployment
Problem: Different configs for dev, staging, and production, but you want one codebase.
Solution:
# Development: .env file
# Staging: Environment variables
# Production: etcd + environment variables
cfg = Config(
model=AppConfig,
sources=[
sources.DotEnv(".env"), # Dev only (file may not exist in prod)
sources.Env(), # All environments
sources.Etcd.from_env() if os.getenv("ETCD_HOST") else None, # Prod only
sources.CLI(), # Override for debugging
],
)
Benefits:
- โ One codebase, multiple environments
- โ Environment-specific sources automatically handled
- โ Clear priority: CLI > etcd > env > .env > defaults
Use Case 4: Complex Nested Configuration
Problem: Your config has nested structures (database, cache, API keys, etc.).
Solution:
@dataclass(frozen=True)
class DatabaseConfig:
host: str = field(default="localhost")
port: int = field(default=5432)
name: str = field(default="mydb")
@dataclass(frozen=True)
class AppConfig:
db: DatabaseConfig = field(default_factory=DatabaseConfig)
api_key: str = field()
cache_ttl: int = field(default=3600)
cfg = Config(
model=AppConfig,
sources=[
sources.Env(), # Reads DB__HOST, DB__PORT, DB__NAME automatically
sources.CLI(), # Reads --db-host, --db-port, etc.
],
)
config = cfg.load()
# Access: config.db.host, config.db.port, config.api_key
Benefits:
- โ
Automatic nested key mapping (
DB__HOSTโdb.host) - โ Type-safe nested access
- โ Validation at all levels
๐จ Key Features
1. Multiple Sources, Unified Interface
sources = [
sources.Defaults(), # From model defaults (automatic)
sources.Env(), # From environment variables
sources.CLI(), # From command-line arguments
sources.DotEnv(".env"), # From .env files
sources.YAML("config.yaml"), # From YAML files
sources.TOML("config.toml"), # From TOML files
sources.Etcd(...), # From etcd (optional)
]
2. Simple Priority Rule
Later sources override earlier ones. That's it.
cfg = Config(
model=AppConfig,
sources=[
sources.Env(), # Priority 1 (lowest)
sources.CLI(), # Priority 2 (highest - overrides env)
],
)
3. Automatic Type Conversion
# Environment variables are strings, but Varlord converts them automatically
export PORT=9000 DEBUG=true TIMEOUT=30.5
@dataclass(frozen=True)
class Config:
port: int = 8000 # "9000" โ 9000
debug: bool = False # "true" โ True
timeout: float = 30.0 # "30.5" โ 30.5
4. Model-Driven Filtering
# Your model defines what config you need
@dataclass(frozen=True)
class Config:
host: str = "127.0.0.1"
port: int = 8000
# ... only these fields
# Sources automatically filter - no prefix management needed
# Env source only reads HOST and PORT, ignores everything else
# CLI source only parses --host and --port, ignores other args
5. Built-in Validation
from varlord.validators import validate_range, validate_regex
@dataclass(frozen=True)
class Config:
port: int = field(default=8000)
host: str = field(default="127.0.0.1")
def __post_init__(self):
validate_range(self.port, min=1, max=65535)
validate_regex(self.host, r'^\d+\.\d+\.\d+\.\d+$')
6. Dynamic Updates (Optional)
store = cfg.load_store() # Enable watch if sources support it
store.subscribe(lambda new_config, diff: print(f"Updated: {diff}"))
# Config updates automatically in background
# Thread-safe access: current = store.get()
๐ Documentation
- ๐ Full Documentation: https://varlord.readthedocs.io
- ๐ Quick Start Guide: Quick Start
- ๐ก Examples: Examples Directory
- ๐ฏ API Reference: API Documentation
๐ง Memory Aids (Quick Reference)
The Varlord Mantra
"Define once, use everywhere. Later overrides earlier. Types are automatic."
Priority Cheat Sheet
Defaults < .env < Environment < YAML/TOML < etcd < CLI
(lowest priority) (highest priority)
Common Patterns
# Pattern 1: Simple (most common)
Config(model=AppConfig, sources=[sources.Env(), sources.CLI()])
# Pattern 2: With .env file
Config(model=AppConfig, sources=[sources.DotEnv(".env"), sources.Env(), sources.CLI()])
# Pattern 3: Dynamic updates
Config(model=AppConfig, sources=[sources.Env(), sources.Etcd(..., watch=True)])
store = cfg.load_store()
# Pattern 4: One-liner
Config.from_model(AppConfig, cli=True, dotenv=".env")
๐ข Production Proven
Varlord is part of the Agentsmith ecosystem, battle-tested in production environments:
- โ Deployed in multiple highway management companies
- โ Used by securities firms and regulatory agencies
- โ Handles high-throughput microservices
- โ Thread-safe and production-ready
๐ Agentsmith Open-Source Projects
- Varlord โ๏ธ - Configuration management (this project)
- Routilux โก - Event-driven workflow orchestration
- Serilux ๐ฆ - Flexible serialization framework
- Lexilux ๐ - Unified LLM API client
๐ค Contributing
We welcome contributions! See CONTRIBUTING.md for guidelines.
๐ License
Licensed under the Apache License 2.0. See LICENSE for details.
๐ฏ TL;DR
Varlord solves configuration management once and for all:
- โ Define your config as a dataclass - type-safe, validated
- โ Add sources in priority order - later overrides earlier
- โ
Call
load()- get a type-safe config object - โ Optional: Enable dynamic updates - zero-downtime config changes
No more parsing. No more type conversion. No more priority confusion.
# Before: 50+ lines of parsing, type conversion, validation
# After: 3 lines
cfg = Config(model=AppConfig, sources=[sources.Env(), sources.CLI()])
app = cfg.load()
That's the Varlord promise. ๐
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file varlord-0.8.0.tar.gz.
File metadata
- Download URL: varlord-0.8.0.tar.gz
- Upload date:
- Size: 198.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6d904070527eacce14897f7c9a7f377f3679281994417db27211f714edc8e8d7
|
|
| MD5 |
e7aab226b22d811d952bf6ddaf7fb99c
|
|
| BLAKE2b-256 |
31f1775fba899bf2df5aa7a5ed4da8a09ea4dac70beffdc2977d96829b40e568
|
File details
Details for the file varlord-0.8.0-py3-none-any.whl.
File metadata
- Download URL: varlord-0.8.0-py3-none-any.whl
- Upload date:
- Size: 71.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9f132c99e663f8596bc19e8b87986d0f30bb87f2f33a78af53bd7d51b693d5da
|
|
| MD5 |
ac3a26cdb2cb6b02c8d01f6d137d616d
|
|
| BLAKE2b-256 |
66b3a80b548408f30d3da6ad5dcc6d26c0f9e6bac27c9001d4d883e215e7f960
|