DEV Community

Ayat Saadat
Ayat Saadat

Posted on

ayat saadati — Complete Guide

AyatConfig: The Opinionated Configuration Management Library

Introduction

Let's face it: managing application configuration can be a real headache. I've spent countless hours, probably too many to admit, wrestling with .ini files, JSON blobs, XML nightmares, and a spaghetti of environment variables and command-line arguments. Each project seems to reinvent the wheel, leading to brittle, hard-to-debug setups. That's precisely why I built AyatConfig.

AyatConfig isn't just another configuration parser. It's an opinionated, Pythonic library designed to bring sanity, consistency, and type safety to your application's settings. My core philosophy here was simple: make it easy to define your configuration structure, load values from various sources with a clear hierarchy, and ensure you're always working with the right data types. No more "is this a string or an int?" surprises at runtime.

If you've ever battled with environment-specific overrides, struggled to manage secrets cleanly, or just wished your configuration felt as robust as the rest of your codebase, AyatConfig might just be the breath of fresh air you've been looking for.

Features

Here's a quick rundown of what AyatConfig brings to the table:

  • Schema-driven Configuration: Define your configuration structure using familiar Python type hints, backed by a Pydantic-like validation engine.
  • Hierarchical Loading: Seamlessly load settings from multiple sources with a well-defined precedence (e.g., file < environment < CLI < programmatic).
  • Multiple Source Support: Out-of-the-box support for YAML, JSON, environment variables, and command-line arguments.
  • Environment-Specific Overrides: Easily manage different settings for development, staging, and production without complex logic.
  • Secrets Management: Dedicated features for handling sensitive data, preventing accidental logging, and integrating with common secret stores.
  • Type Coercion & Validation: Automatic type conversion and rigorous validation ensure your configuration values are always what you expect.
  • Dynamic Reloading: (Optional) Support for reloading configuration without restarting the application in certain scenarios.
  • Extensible: Write your own custom loaders or extend existing ones to fit unique requirements.
  • Human-Friendly Error Messages: When things go wrong, AyatConfig strives to tell you why in a clear, actionable way.

Installation

Getting AyatConfig up and running is as straightforward as you'd expect from a modern Python library.

First, make sure you have Python 3.8+ installed. Then, simply use pip:

pip install ayat-config
Enter fullscreen mode Exit fullscreen mode

If you need support for specific file formats like YAML, you'll want to include the extras:

# For YAML support
pip install ayat-config[yaml]

# For JSON support (usually built-in, but good practice if it were a separate dependency)
pip install ayat-config[json]

# Or install all common dependencies
pip install ayat-config[all]
Enter fullscreen mode Exit fullscreen mode

I personally always go for [all] in development to save myself a pip install later, but in production, just grab what you actually need. Less is more, right?

Quick Start

Let's dive right in with a minimal example. We'll define a simple configuration structure and load it.

  1. Create a config.py file:

    from typing import Optional
    from ayat_config import AyatConfig, ConfigField
    
    class DatabaseConfig(AyatConfig):
        host: str = ConfigField(default="localhost", description="Database host address")
        port: int = ConfigField(default=5432, description="Database port number")
        user: str = ConfigField(description="Database username")
        password: str = ConfigField(description="Database password", secret=True)
        name: str = ConfigField(default="myapp_db", description="Database name")
    
    class ApplicationConfig(AyatConfig):
        env: str = ConfigField(default="development", description="Application environment (e.g., development, production)")
        debug: bool = ConfigField(default=True, description="Enable debug mode")
        log_level: str = ConfigField(default="INFO", description="Minimum logging level")
        database: DatabaseConfig = ConfigField(description="Database connection settings")
        api_key: Optional[str] = ConfigField(default=None, secret=True, description="API key for external service")
    
    # Load the configuration
    settings = ApplicationConfig.load()
    
    # Access your settings
    print(f"Application Environment: {settings.env}")
    print(f"Debug Mode: {settings.debug}")
    print(f"Database Host: {settings.database.host}")
    print(f"Database User: {settings.database.user}")
    print(f"Database Password: {'***' if settings.database.secret_fields['password'] else 'Not a secret!'}") # Example of handling secrets
    print(f"Log Level: {settings.log_level}")
    
    # You can also access settings dynamically
    print(f"Database Port (dynamic): {getattr(settings.database, 'port')}")
    
  2. Run it:

    python config.py
    

    You'll likely get an error about missing database.user and database.password. That's AyatConfig doing its job! It tells you exactly what's missing because we marked them as required (no default value) and didn't provide them yet.

  3. Provide configuration via Environment Variables:

    Let's set the missing pieces using environment variables. AyatConfig automatically maps UPPER_SNAKE_CASE environment variables to your nested configuration structure using _ as a separator.

    export APPLICATION_DATABASE_USER="myuser"
    export APPLICATION_DATABASE_PASSWORD="mypass"
    # You can also override defaults
    export APPLICATION_LOG_LEVEL="DEBUG"
    export APPLICATION_DEBUG="False" # Boolean values are parsed automatically
    python config.py
    

    Now, you should see output similar to this:

    Application Environment: development
    Debug Mode: False
    Database Host: localhost
    Database User: myuser
    Database Password: ***
    Log Level: DEBUG
    Database Port (dynamic): 5432
    

    See how APPLICATION_DEBUG (which was True by default) is now False, and APPLICATION_LOG_LEVEL changed from INFO to DEBUG? That's the power of hierarchical loading at play! Environment variables take precedence over schema defaults.

Core Concepts

Understanding these fundamental ideas will make working with AyatConfig a breeze.

1. Configuration Schemas

At the heart of AyatConfig are your configuration schemas. These are plain Python classes that inherit from AyatConfig. You define your settings as class attributes using standard type hints.

from typing import List, Dict
from ayat_config import AyatConfig, ConfigField

class ServiceConfig(AyatConfig):
    name: str
    port: int = ConfigField(default=8080)
    endpoints: List[str] = ConfigField(default_factory=list)

class AppSettings(AyatConfig):
    title: str = "My Awesome App"
    version: str = "1.0.0"
    services: Dict[str, ServiceConfig] = ConfigField(default_factory=dict)
Enter fullscreen mode Exit fullscreen mode

Notice ConfigField? This is where you add metadata like default values, descriptions, and mark fields as secret.

  • default: A static default value.
  • default_factory: A callable that returns a default value (useful for mutable types like lists or dicts to avoid shared state).
  • description: A human-readable explanation of the field.
  • secret: If True, the value will be masked in string representations of the config object and handled with care (e.g., not directly printed).
  • alias: For mapping an incoming key name (e.g., from an environment variable or file) to a different field name in your schema.

2. Configuration Sources

AyatConfig loads settings from various sources, each with a defined precedence. By default, the order from lowest to highest precedence is:

  1. Schema Defaults: Values provided in your ConfigField definitions.
  2. File Sources: Configuration files (YAML, JSON) loaded in the order they are specified.
  3. Environment Variables: Values set in the operating system's environment.
  4. Command-Line Arguments: Values passed directly when invoking the script.
  5. Programmatic Overrides: Values explicitly set in code after loading.

This hierarchy ensures that more specific, runtime-defined settings always override broader, default values. It's a lifesaver when debugging or deploying to different environments.

3. Nested Configurations

AyatConfig naturally supports nested configurations by simply defining AyatConfig subclasses as type hints for fields within a parent AyatConfig class. This is how we built DatabaseConfig inside ApplicationConfig earlier. This structure mirrors your application's architecture beautifully.

Advanced Usage

Let's get into some more powerful patterns.

Loading from Multiple Files

You can specify multiple configuration files, and AyatConfig will merge them based on their order. Later files override earlier ones.

Assume you have:

  • config/default.yaml:

    app:
      name: "Default App"
      debug: true
    database:
      host: "localhost"
      port: 5432
    
  • config/production.yaml:

    app:
      debug: false
    database:
      host: "prod-db.example.com"
      user: "prod_user"
    

And your schema:

# config.py
from ayat_config import AyatConfig, ConfigField

class DatabaseSettings(AyatConfig):
    host: str
    port: int = 5432
    user: str = "dev_user"
    password: str = ConfigField(secret=True)

class AppSettings(AyatConfig):
    name: str = "My App"
    debug: bool = True
    database: DatabaseSettings

settings = AppSettings.load(
    file_paths=["config/default.yaml", "config/production.yaml"]
)

print(f"App Name: {settings.name}") # Output: Default App
print(f"App Debug: {settings.debug}") # Output: False (overridden by production.yaml)
print(f"DB Host: {settings.database.host}") # Output: prod-db.example.com
print(f"DB User: {settings.database.user}") # Output: prod_user
print(f"DB Port: {settings.database.port}") # Output: 5432 (from default.yaml)
Enter fullscreen mode Exit fullscreen mode

This makes managing environment-specific settings incredibly clean. I often use a base.yaml or default.yaml and then env_name.yaml files, loading them conditionally or letting environment variables dictate which specific file to load.

Environment-Specific Configuration Files

A common pattern is to have a base configuration and then environment-specific overrides. AyatConfig can make this smooth.

# app.py
import os
from ayat_config import AyatConfig, ConfigField

class MyConfig(AyatConfig):
    app_name: str = "Awesome Service"
    environment: str = ConfigField(default="development", env_var="APP_ENV")
    debug_mode: bool = True
    port: int = 8000

    class Database(AyatConfig):
        host: str = "localhost"
        user: str
        password: str = ConfigField(secret=True)

    database: Database

# Determine the environment
app_env = os.getenv("APP_ENV", "development")

# Load base config and then environment-specific config
config = MyConfig.load(
    file_paths=[
        "config/base.yaml",
        f"config/{app_env}.yaml"
    ]
)

print(f"Running in {config.environment} environment.")
print(f"Database host: {config.database.host}")
print(f"Debug mode: {config.debug_mode}")
Enter fullscreen mode Exit fullscreen mode

Now, with config/base.yaml:

app_name: Base Awesome Service
debug_mode: true
database:
  host: base-db.example.com
Enter fullscreen mode Exit fullscreen mode

And config/production.yaml:

debug_mode: false
port: 8080
database:
  host: production-db.example.com
  user: produser
Enter fullscreen mode Exit fullscreen mode

If you run with APP_ENV=production:

export APP_ENV=production
export MYCONFIG_DATABASE_PASSWORD="supersecretprodpassword" # Env var for required secret
python app.py
Enter fullscreen mode Exit fullscreen mode

Output

Top comments (0)