Configuration Classes
Configuration classes are at the heart of Configr. They define the structure of your configuration using Python's dataclasses and type hints, providing a type-safe approach to configuration management.
Basic Configuration Class
At its simplest, a configuration class is just a dataclass decorated with @config_class:
from configr import config_class
@config_class
class DatabaseConfig:
username: str
password: str
database: str
host: str = 'localhost'
port: int = 5432
This class defines a configuration with five fields:
host: A string with a default value of localhost (optional)port: An integer with a default value of 5432 (optional)username: A string with no default value (required)password: A string with no default value (required)database: A string with no default value (required)
The @config_class Decorator
The @config_class decorator does several things:
- It ensures the class is a dataclass (converts it if it's not already)
- It adds a
_config_file_nameattribute to specify which file to load - It sets up the class for proper loading by ConfigBase
Specifying a Custom File Name
By default, Configr will use the snake_case version of the class name as the file name. For example, DatabaseConfig
will look for database_config. It will try different extensions based on available loaders (e.g., .json, .yaml,
.yml).
You can specify a custom file name:
@config_class(file_name="db_settings.yaml")
class DatabaseConfig:
host: str = 'localhost'
port: int = 5432
# ...
Now Configr will look for _config/db_settings.yaml.
Using with Existing Dataclasses
If your class is already a dataclass, the decorator will preserve that:
from dataclasses import dataclass
from configr import config_class
@config_class # This works
@dataclass
class AppConfig:
debug: bool = False
log_level: str = "INFO"
# Or this way:
@dataclass
@config_class # This also works
class ServerConfig:
host: str = 'localhost'
port: int = 8080
Type Safety and Validation
Configr leverages Python's type hints to provide type safety for your configuration.
Basic Types
You can use all standard Python types:
@config_class
class AppConfig:
name: str
version: str
port: int
debug: bool
rate_limit: float
tags: list
options: dict
handler: callable = None
Type Validation
Configr performs strict type validation when loading configuration, ensuring that the values in your configuration file match the expected types:
# _config/app_config.json
{
"name": "MyApp",
"version": "1.0.0",
"port": "8080", # This is a string, not an int!
"debug": true,
"rate_limit": 100.0,
"tags": ["tag1", "tag2"],
"options": {}
}
# This will raise a ConfigValidationError when instantiating the dataclass since "port" is a string but should be an int
config = ConfigBase.load(AppConfig)
To add custom validation, use the __post_init__ method in your config class:
from configr import config_class, ConfigValidationError
@config_class
class ServerConfig:
host: str
port: int
def __post_init__(self):
if self.port < 1024 or self.port > 65535:
raise ConfigValidationError(f"Invalid port: {self.port}")
Default Values
Specify default values for optional configuration parameters:
@config_class
class AppConfig:
debug: bool = False
log_level: str = "INFO"
max_connections: int = 100
timeout: int = 30
Fields without default values are considered required and must be provided in the configuration file.
Inheritance
You can use inheritance to create specialized configuration classes:
@config_class
class BaseConfig:
debug: bool = False
log_level: str = "INFO"
@config_class
class DevelopmentConfig(BaseConfig):
debug: bool = True
database_url: str = "sqlite:///dev.db"
@config_class
class ProductionConfig(BaseConfig):
log_level: str = "WARNING"
database_url: str = "postgresql://user:pass@localhost/prod"
Configuration Class Methods
You can add methods to your configuration classes for convenience:
@config_class
class DatabaseConfig:
driver: str
host: str
port: int
username: str
password: str
database: str
def get_connection_string(self):
"""Generate a database connection string."""
return f"{self.driver}://{self.username}:{self.password}@{self.host}:{self.port}/{self.database}"
def get_connection_params(self):
"""Return connection parameters as a dictionary."""
return {
"driver": self.driver,
"host": self.host,
"port": self.port,
"username": self.username,
"password": self.password,
"database": self.database
}
Nested Configuration
Configr provides robust support for nested configuration structures, allowing you to organize complex configurations in a clean, type-safe manner. The library automatically handles the conversion between nested JSON/YAML structures and Python dataclasses.
Using Nested Dataclasses
You can define nested configuration structures by using dataclasses as field types within your config classes:
from configr import config_class
from dataclasses import dataclass
# Define nested dataclass for database configuration
@dataclass
class DatabaseConfig:
host: str = "localhost"
port: int = 5432
username: str = None
password: str = None
database: str = None
# Define nested dataclass for logging configuration
@dataclass
class LoggingConfig:
level: str = "INFO"
file: str = None
format: str = "%(asctime)s - %(levelname)s - %(message)s"
# Main configuration class using nested dataclasses
@config_class(file_name="app_config.json")
class AppConfig:
name: str
version: str
debug: bool = False
database: DatabaseConfig = None # Nested dataclass field
logging: LoggingConfig = None # Nested dataclass field
With a corresponding JSON file like:
{
"name": "MyApp",
"version": "1.0.0",
"debug": true,
"database": {
"host": "db.example.com",
"port": 5432,
"username": "admin",
"password": "secure_password",
"database": "myapp_db"
},
"logging": {
"level": "DEBUG",
"file": "app.log"
}
}
When you load the configuration, Configr will automatically:
- Recognize that
databaseis a field of typeDatabaseConfig - Convert the nested JSON object to a
DatabaseConfiginstance - Similarly convert the
loggingfield to aLoggingConfiginstance - Handle any level of nesting recursively
Accessing nested configuration is done with standard dot notation:
from configr import ConfigBase
# Load the configuration
config = ConfigBase.load(AppConfig)
# Access nested fields with dot notation
db_host = config.database.host # "db.example.com"
log_level = config.logging.level # "DEBUG"
log_format = config.logging.format # Uses default value
Collections of Dataclasses
Configr also supports collections of dataclasses, such as lists or dictionaries of dataclass instances. This is useful for configuration items that can have multiple instances or variations.
Lists of Dataclasses
from configr import config_class
from dataclasses import dataclass
@dataclass
class ServiceConfig:
name: str
url: str
timeout: int = 30
retries: int = 3
@config_class(file_name="services_config.json")
class ServicesConfig:
enabled: bool = True
services: list[ServiceConfig] = None # List of dataclass instances
With a JSON file like:
{
"enabled": true,
"services": [
{
"name": "authentication",
"url": "https://auth.example.com/api",
"timeout": 10
},
{
"name": "storage",
"url": "https://storage.example.com/api",
"timeout": 60,
"retries": 5
}
]
}
Configr will automatically convert each object in the services list to a ServiceConfig instance:
config = ConfigBase.load(ServicesConfig)
# Access the first service
auth_service = config.services[0]
print(auth_service.name) # "authentication"
print(auth_service.timeout) # 10
print(auth_service.retries) # 3 (default value)
# Iterate through all services
for service in config.services:
print(f"{service.name}: {service.url}")
Default Values and None Handling
Configr intelligently handles default values in nested dataclasses:
- If a nested dataclass field is
Nonein the configuration file, Configr will attempt to create an empty instance using the class's default constructor. - If the nested dataclass constructor requires arguments with no defaults, the field will remain
None. - Default values in nested dataclasses are respected at all levels of nesting.
Example:
@dataclass
class DatabaseConfig:
# These fields have no defaults and are required
username: str
password: str
database: str
host: str = "localhost"
port: int = 5432
@config_class
class AppConfig:
debug: bool = False
# This will be None if not in the config file, since DatabaseConfig
# has required fields with no defaults
database: DatabaseConfig = None
Type Validation in Nested Structures
Configr performs type validation for nested dataclass fields just like it does for top-level fields. This is handled by
the FieldTypeChecker class which ensures that your entire configuration hierarchy maintains type safety.
Configr will raise a ConfigValidationError if a type check for a field fails.
Customizing Nested Configuration
You can add methods to your nested dataclasses for additional functionality:
@dataclass
class DatabaseConfig:
host: str
port: int
username: str
password: str
database: str
def get_connection_string(self):
"""Generate a database connection string."""
return f"postgresql://{self.username}:{self.password}@{self.host}:{self.port}/{self.database}"
def __post_init__(self):
"""Validate database configuration."""
if self.port < 1024 or self.port > 65535:
raise ValueError(f"Invalid port: {self.port}")
Then use these methods in your code:
Best Practices for Nested Configuration
-
Keep nesting reasonable: While Configr supports arbitrary nesting, keep your hierarchy sensible for maintainability.
-
Use explicit types: Always use explicit type annotations, especially for collections of dataclasses.
-
Provide defaults where appropriate: Use default values for optional fields to make your configuration more robust.
-
Add validation in
__post_init__: Add custom validation in the__post_init__method of your dataclasses. -
Break complex configurations into logical modules: For very complex configurations, consider splitting your config classes across multiple modules.
Environment-Specific Configuration
For environment-specific configuration:
import os
ENV = os.environ.get("ENV", "development")
@config_class(file_name=f"app.{ENV}.json")
class AppConfig:
debug: bool = ENV != "production"
log_level: str = "DEBUG" if ENV != "production" else "INFO"
# ...
Best Practices
When working with configuration classes:
-
Use Meaningful Default Values: Provide sensible defaults whenever possible
-
Add Validation Logic: Use
__post_init__to validate configuration beyond the type checking offered by Configr -
Organize Related Settings: Group related settings in separate classes
-
Use Strong Typing: Leverage Python's type hints for better code quality
-
Manage Secrets Carefully: Consider separating sensitive information from regular configuration
Next Steps
Now that you understand how to work with configuration classes in Configr, you might want to explore:
- Custom Loaders to learn how to extend Configr with support for additional file formats