Skip to main content

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 โš™๏ธ

PyPI version Python 3.8+ License Documentation CI codecov

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?

  1. โœ… Multiple Sources, Unified Interface: System config, app config, user config, env vars, CLI - all handled the same way
  2. โœ… Clear Priority: Later sources override earlier ones - no confusion
  3. โœ… Automatic Type Conversion: Strings from files/env โ†’ proper types (int, bool, float)
  4. โœ… Model-Driven Filtering: Each source only reads fields defined in your model
  5. โœ… Built-in Diagnostics: --check-variables shows exactly what's loaded from where
  6. โœ… 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 -cv shows 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? โ†’ -cv shows 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

  1. ๐ŸŽฏ Model-Driven Design: Define your config once as a dataclass, and Varlord handles the rest
  2. ๐Ÿ”„ Smart Auto-Filtering: Sources automatically filter by model fields - no prefix management needed
  3. โšก Zero Boilerplate: Model defaults are automatic, model is auto-injected to sources
  4. ๐Ÿ›ก๏ธ Type Safety First: Full type hints support with automatic conversion and validation
  5. ๐Ÿš€ 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_workers is always an int, never a string
  • โœ… Validation: Missing api_key fails 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


๐Ÿง  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:

  1. โœ… Define your config as a dataclass - type-safe, validated
  2. โœ… Add sources in priority order - later overrides earlier
  3. โœ… Call load() - get a type-safe config object
  4. โœ… 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


Download files

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

Source Distribution

varlord-0.8.0.tar.gz (198.4 kB view details)

Uploaded Source

Built Distribution

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

varlord-0.8.0-py3-none-any.whl (71.2 kB view details)

Uploaded Python 3

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

Hashes for varlord-0.8.0.tar.gz
Algorithm Hash digest
SHA256 6d904070527eacce14897f7c9a7f377f3679281994417db27211f714edc8e8d7
MD5 e7aab226b22d811d952bf6ddaf7fb99c
BLAKE2b-256 31f1775fba899bf2df5aa7a5ed4da8a09ea4dac70beffdc2977d96829b40e568

See more details on using hashes here.

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

Hashes for varlord-0.8.0-py3-none-any.whl
Algorithm Hash digest
SHA256 9f132c99e663f8596bc19e8b87986d0f30bb87f2f33a78af53bd7d51b693d5da
MD5 ac3a26cdb2cb6b02c8d01f6d137d616d
BLAKE2b-256 66b3a80b548408f30d3da6ad5dcc6d26c0f9e6bac27c9001d4d883e215e7f960

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