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, andproductionwithout 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,
AyatConfigstrives 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
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]
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.
-
Create a
config.pyfile:
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')}") -
Run it:
python config.pyYou'll likely get an error about missing
database.useranddatabase.password. That'sAyatConfigdoing its job! It tells you exactly what's missing because we marked them as required (no default value) and didn't provide them yet. -
Provide configuration via Environment Variables:
Let's set the missing pieces using environment variables.
AyatConfigautomatically mapsUPPER_SNAKE_CASEenvironment 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.pyNow, 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): 5432See how
APPLICATION_DEBUG(which wasTrueby default) is nowFalse, andAPPLICATION_LOG_LEVELchanged fromINFOtoDEBUG? 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)
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: IfTrue, 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:
- Schema Defaults: Values provided in your
ConfigFielddefinitions. - File Sources: Configuration files (YAML, JSON) loaded in the order they are specified.
- Environment Variables: Values set in the operating system's environment.
- Command-Line Arguments: Values passed directly when invoking the script.
- 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)
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}")
Now, with config/base.yaml:
app_name: Base Awesome Service
debug_mode: true
database:
host: base-db.example.com
And config/production.yaml:
debug_mode: false
port: 8080
database:
host: production-db.example.com
user: produser
If you run with APP_ENV=production:
export APP_ENV=production
export MYCONFIG_DATABASE_PASSWORD="supersecretprodpassword" # Env var for required secret
python app.py
Output
Top comments (0)