Python Snippets

Smart Configuration Manager with Environment-Based Loading

import os
import json
from typing import Any, Dict, Optional
from pathlib import Path

class ConfigManager:
    """
    A smart configuration manager that loads settings from multiple sources
    with environment-based overrides and type validation.
    """
    
    def __init__(self, config_file: str = "config.json"):
        self.config_file = config_file
        self._config = {}
        self._load_config()
    
    def _load_config(self):
        """Load configuration from file and environment variables"""
        # Load from JSON file
        if os.path.exists(self.config_file):
            with open(self.config_file, 'r') as f:
                file_config = json.load(f)
                self._config.update(file_config)
        
        # Load environment variables with prefix
        env_prefix = "APP_"
        for key, value in os.environ.items():
            if key.startswith(env_prefix):
                config_key = key[len(env_prefix):].lower()
                # Try to parse as JSON for complex types, fallback to string
                try:
                    self._config[config_key] = json.loads(value)
                except json.JSONDecodeError:
                    self._config[config_key] = value
    
    def get(self, key: str, default: Any = None, value_type: type = None) -> Any:
        """
        Get configuration value with optional type conversion and default value.
        
        Args:
            key: Configuration key
            default: Default value if key not found
            value_type: Type to convert the value to (int, float, bool, list, dict)
        
        Returns:
            Configuration value
        """
        value = self._config.get(key, default)
        
        if value_type is not None and value is not None:
            try:
                if value_type == bool:
                    if isinstance(value, str):
                        return value.lower() in ('true', '1', 'yes', 'on')
                    return bool(value)
                elif value_type == list:
                    if isinstance(value, str):
                        return json.loads(value)
                    return list(value)
                elif value_type == dict:
                    if isinstance(value, str):
                        return json.loads(value)
                    return dict(value)
                else:
                    return value_type(value)
            except (ValueError, TypeError, json.JSONDecodeError):
                # Return default if conversion fails
                return default
        
        return value
    
    def get_nested(self, key_path: str, separator: str = '.', default: Any = None) -> Any:
        """
        Get nested configuration value using dot notation.
        
        Args:
            key_path: Dot-separated path to nested value (e.g., 'database.host')
            separator: Separator for nested keys
            default: Default value if key not found
        
        Returns:
            Nested configuration value
        """
        keys = key_path.split(separator)
        current = self._config
        
        try:
            for key in keys:
                current = current[key]
            return current
        except (KeyError, TypeError):
            return default
    
    def reload(self):
        """Reload configuration from sources"""
        self._config = {}
        self._load_config()
    
    def get_all(self) -> Dict[str, Any]:
        """Get all configuration values"""
        return self._config.copy()

# Example usage
if __name__ == "__main__":
    # Create sample config file
    sample_config = {
        "database": {
            "host": "localhost",
            "port": 5432,
            "username": "user",
            "password": "secret"
        },
        "api": {
            "timeout": 30,
            "retries": 3
        },
        "debug": False,
        "features": ["auth", "logging"]
    }
    
    with open("config.json", "w") as f:
        json.dump(sample_config, f, indent=2)
    
    # Initialize config manager
    config = ConfigManager()
    
    # Usage examples
    print("Database host:", config.get_nested("database.host"))
    print("API timeout:", config.get("timeout", 30, int))
    print("Debug mode:", config.get("debug", False, bool))
    print("Features:", config.get("features", [], list))
    
    # Clean up
    os.remove("config.json")

This code snippet provides a comprehensive configuration manager that handles one of the most common challenges in application development: managing configuration settings across different environments. It’s especially useful in modern cloud deployments and microservices architectures.

Key Features and Benefits:

  1. Multi-source Configuration: Loads settings from both JSON files and environment variables, allowing flexibility for different deployment scenarios
  2. Environment Variable Prefix: Uses APP_ prefix to avoid conflicts with system variables
  3. Type Safety: Automatically converts values to appropriate types (int, float, bool, list, dict) with fallback handling
  4. Nested Configuration Access: Allows dot notation access to deeply nested configuration values
  5. Dynamic Reloading: Can reload configuration when settings change
  6. Type Conversion: Safely converts environment variables to proper data types
  7. Error Handling: Gracefully handles missing or invalid configuration values

Why This is Useful:

How to Run:

  1. Save the code to a file (e.g., config_manager.py)
  2. Create a config.json file with your application settings (optional)
  3. Set environment variables with APP_ prefix (e.g., APP_DEBUG=true)
  4. Run the script or import the ConfigManager class in your application

The manager will automatically load settings from both sources, with environment variables taking precedence over file-based configuration. This pattern is ideal for 12-factor applications where configuration is stored in the environment.