Configuration Management¶
Multi-source configuration with automatic validation and type safety.
Overview¶
The configuration module provides a flexible, type-safe approach to loading configuration from multiple sources with automatic validation using Pydantic.
Key Features¶
- Multi-source loading: Combine defaults, files, environment variables, and secrets
- Type-safe: Automatic validation with Pydantic models
- Format support: YAML, JSON, TOML, HOCON, ENV files
- Deep merging: Later sources override earlier ones
- Environment overrides: Production values via environment variables
- Optional sources: Development overrides without breaking production
Basic Usage¶
Define Configuration Model¶
from pydantic import BaseModel, Field
class DatabaseConfig(BaseModel):
host: str = "localhost"
port: int = 5432
name: str
password: str
class AppConfig(BaseModel):
debug: bool = False
database: DatabaseConfig
api_key: str
Load Configuration¶
from dspu.config import Config
# Simple: from single file
config = Config.load_from_file(AppConfig, "config.yaml")
# Advanced: from multiple sources
from dspu.config import FileSource, EnvSource
config = Config.load(
AppConfig,
sources=[
FileSource("config.yaml"), # Base configuration
EnvSource(prefix="APP_"), # Environment overrides
],
)
Configuration Sources¶
FileSource¶
Load from configuration files:
from dspu.config import FileSource
# YAML
FileSource("config.yaml")
# JSON
FileSource("config.json")
# TOML
FileSource("config.toml")
# HOCON
FileSource("application.conf")
# Optional file (won't error if missing)
FileSource("config.local.yaml", required=False)
# Explicit format
FileSource("settings.txt", format="json")
EnvSource¶
Load from environment variables:
from dspu.config import EnvSource
# With prefix
EnvSource(prefix="APP_") # Reads APP_DEBUG, APP_API_KEY, etc.
# Nested keys with separator
EnvSource(prefix="APP_", separator="__")
# APP_DATABASE__HOST → config.database.host
# APP_DATABASE__PORT → config.database.port
# No prefix (use all env vars)
EnvSource()
DictSource¶
Load from dictionary:
from dspu.config import DictSource
defaults = {
"debug": False,
"database": {
"host": "localhost",
"port": 5432,
}
}
DictSource(defaults)
Source Priority¶
Later sources override earlier ones:
config = Config.load(
AppConfig,
sources=[
DictSource(defaults), # 1. Defaults (lowest priority)
FileSource("config.yaml"), # 2. Base config
FileSource(f"config.{env}.yaml"), # 3. Environment-specific
FileSource("config.local.yaml", required=False), # 4. Local overrides
EnvSource(prefix="APP_"), # 5. Environment variables (highest)
],
)
Example:
# config.yaml
debug: false
api_key: dev_key_123
# config.production.yaml
debug: false
api_key: prod_key_456
# Environment
export APP_DEBUG=true
export APP_API_KEY=runtime_key_789
Result: debug=true, api_key=runtime_key_789 (env vars win)
Deep Merging¶
Nested configurations are merged deeply:
# Source 1
{
"database": {
"host": "localhost",
"port": 5432,
}
}
# Source 2
{
"database": {
"port": 3306, # Override port
"name": "mydb", # Add name
}
}
# Result
{
"database": {
"host": "localhost", # Kept from source 1
"port": 3306, # Overridden by source 2
"name": "mydb", # Added by source 2
}
}
Validation¶
Field Constraints¶
Use Pydantic's validation features:
from pydantic import BaseModel, Field, EmailStr, HttpUrl
class AppConfig(BaseModel):
# Range validation
port: int = Field(ge=1, le=65535)
workers: int = Field(ge=1, le=100)
# String validation
email: EmailStr
api_url: HttpUrl
api_key: str = Field(min_length=32)
# Enum validation
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR"]
Custom Validators¶
from pydantic import field_validator
class AppConfig(BaseModel):
database_url: str
@field_validator("database_url")
@classmethod
def validate_database_url(cls, v: str) -> str:
if not v.startswith(("postgresql://", "mysql://")):
raise ValueError("Invalid database URL scheme")
return v
Conditional Validation¶
from pydantic import model_validator
class AppConfig(BaseModel):
use_cache: bool
cache_url: str | None = None
@model_validator(mode='after')
def check_cache_url(self):
if self.use_cache and not self.cache_url:
raise ValueError("cache_url required when use_cache is True")
return self
Environment Variables¶
Naming Convention¶
Use prefixes and separators:
# Flat keys
export APP_DEBUG=true
export APP_API_KEY=secret
# Nested keys (use __)
export APP_DATABASE__HOST=localhost
export APP_DATABASE__PORT=5432
export APP_DATABASE__NAME=mydb
Type Coercion¶
Environment variables are automatically converted:
# Boolean
export APP_DEBUG=true # → True
export APP_DEBUG=1 # → True
export APP_DEBUG=false # → False
export APP_DEBUG=0 # → False
# Integer
export APP_PORT=8080 # → 8080
# List (JSON)
export APP_TAGS='["a","b","c"]' # → ["a", "b", "c"]
# Dict (JSON)
export APP_META='{"key":"value"}' # → {"key": "value"}
Common Patterns¶
Pattern 1: Local Development¶
import os
config = Config.load(
AppConfig,
sources=[
FileSource("config.yaml"), # Base config
FileSource("config.local.yaml", required=False), # Local overrides (gitignored)
EnvSource(prefix="APP_"), # Environment overrides
],
)
Setup:
# .gitignore
config.local.yaml
# config.yaml (committed)
debug: false
database:
host: localhost
# config.local.yaml (not committed)
debug: true
database:
host: docker.local
Pattern 2: Docker Compose¶
docker-compose.yml:
services:
app:
environment:
MYAPP_DEBUG: "true"
MYAPP_DATABASE__HOST: postgres
MYAPP_DATABASE__PASSWORD: secret
Pattern 3: Kubernetes¶
config = Config.load(
AppConfig,
sources=[
FileSource("/config/app.yaml"), # From ConfigMap
EnvSource(prefix="", separator="__"), # From Secrets
],
)
Kubernetes:
# ConfigMap for non-sensitive config
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
app.yaml: |
debug: false
database:
host: postgres-service
# Secret for sensitive data
apiVersion: v1
kind: Secret
metadata:
name: app-secrets
data:
DATABASE__PASSWORD: <base64-encoded>
API_KEY: <base64-encoded>
Pattern 4: Environment-Specific¶
import os
env = os.getenv("ENV", "development")
config = Config.load(
AppConfig,
sources=[
FileSource("config.yaml"), # Base
FileSource(f"config.{env}.yaml"), # Environment-specific
EnvSource(prefix="APP_"), # Runtime overrides
],
)
Files:
config.yaml # Base (committed)
config.development.yaml # Dev overrides (committed)
config.staging.yaml # Staging overrides (committed)
config.production.yaml # Production overrides (committed)
Pattern 5: 12-Factor App¶
# Pure environment variable config
config = Config.load(
AppConfig,
sources=[
EnvSource(), # No prefix - use all env vars
],
)
Watching Configuration¶
Monitor configuration files for changes:
from dspu.config import WatchedConfig
# Create watched config
watched = WatchedConfig(
AppConfig,
sources=[FileSource("config.yaml")],
poll_interval=5.0 # Check every 5 seconds
)
# Start watching
await watched.start()
# Get current config
config = watched.current
# Register callback for changes
def on_config_change(new_config: AppConfig):
print("Configuration changed!")
# Reload application...
watched.on_change(on_config_change)
# Stop watching
await watched.stop()
Best Practices¶
Security¶
✅ DO:
- Use environment variables for secrets
- Use secret managers in production (Vault, AWS Secrets Manager)
- Keep sensitive config out of version control
- Use .gitignore for local config files
❌ DON'T: - Don't commit secrets to git - Don't log sensitive configuration - Don't hardcode credentials - Don't use weak defaults
Organization¶
✅ DO: - Layer configuration (defaults → file → env → secrets) - Use meaningful names and prefixes - Document required vs optional settings - Provide sensible defaults - Validate early with Pydantic
❌ DON'T: - Don't use loose types (use validation) - Don't skip documentation - Don't mix concerns (separate DB, API, etc.) - Don't use magic values
Deployment¶
✅ DO: - Use environment-specific config files - Override with environment variables - Make local overrides optional - Test configuration loading
❌ DON'T: - Don't use same config for all environments - Don't require files in production (use env vars) - Don't skip error handling - Don't hardcode environment-specific values
Troubleshooting¶
ValidationError: Field required¶
Problem: Missing required field
Solution:
# Add default value
class AppConfig(BaseModel):
api_key: str = "default_key" # ✅
# Or make optional
class AppConfig(BaseModel):
api_key: str | None = None # ✅
ConfigurationError: File not found¶
Problem: Required config file missing
Solution:
Environment variables not working¶
Problem: Env vars not being picked up
Solution:
# Check prefix and separator
EnvSource(prefix="APP_", separator="__")
# Env var should be: APP_DATABASE__HOST
# Not: APP_DATABASE_HOST or DATABASE__HOST