Python Snippets

Environment Variable Configuration Loader with Type Casting

import os
from typing import Any, Dict, List, Optional, Union
import json

class EnvConfigLoader:
    """
    A utility class for loading environment variables with automatic type casting.
    Supports strings, integers, floats, booleans, lists, and JSON objects.
    """
    
    @staticmethod
    def _cast_value(value: str, target_type: str) -> Any:
        """Cast string value to the specified type."""
        value = value.strip()
        
        if target_type == 'str':
            return value
        elif target_type == 'int':
            return int(value)
        elif target_type == 'float':
            return float(value)
        elif target_type == 'bool':
            return value.lower() in ('true', '1', 'yes', 'on')
        elif target_type == 'list':
            # Try to parse as JSON array first
            try:
                parsed = json.loads(value)
                if isinstance(parsed, list):
                    return parsed
            except json.JSONDecodeError:
                pass
            # Fall back to comma-separated list
            return [item.strip() for item in value.split(',') if item.strip()]
        elif target_type == 'json':
            try:
                return json.loads(value)
            except json.JSONDecodeError as e:
                raise ValueError(f"Invalid JSON in environment variable: {e}")
        else:
            raise ValueError(f"Unsupported type: {target_type}")
    
    @classmethod
    def load_config(cls, config_schema: Dict[str, Dict[str, Any]]) -> Dict[str, Any]:
        """
        Load configuration from environment variables based on a schema.
        
        Args:
            config_schema: A dictionary defining the configuration schema.
                          Format: {'VAR_NAME': {'type': 'str|int|float|bool|list|json', 
                                                'default': optional_default_value,
                                                'required': True/False}}
        
        Returns:
            Dictionary with loaded configuration values.
        """
        config = {}
        
        for var_name, var_config in config_schema.items():
            var_type = var_config.get('type', 'str')
            default = var_config.get('default', None)
            required = var_config.get('required', False)
            
            # Get value from environment
            value = os.environ.get(var_name, default)
            
            # Handle missing required variables
            if value is None and required:
                raise ValueError(f"Required environment variable '{var_name}' is not set")
            
            # Cast value if present
            if value is not None:
                try:
                    config[var_name] = cls._cast_value(str(value), var_type)
                except Exception as e:
                    raise ValueError(f"Error parsing environment variable '{var_name}': {e}")
            else:
                config[var_name] = None
                
        return config

# Example usage
if __name__ == "__main__":
    # Define configuration schema
    config_schema = {
        'DEBUG': {
            'type': 'bool',
            'default': False,
            'required': False
        },
        'PORT': {
            'type': 'int',
            'default': 8000,
            'required': False
        },
        'DATABASE_URL': {
            'type': 'str',
            'required': True
        },
        'ALLOWED_HOSTS': {
            'type': 'list',
            'default': 'localhost,127.0.0.1',
            'required': False
        },
        'FEATURE_FLAGS': {
            'type': 'json',
            'required': False
        }
    }
    
    # Load configuration (in a real app, environment variables would be set externally)
    # For demonstration, we'll set some test values
    os.environ['DATABASE_URL'] = 'postgresql://user:pass@localhost/mydb'
    os.environ['DEBUG'] = 'true'
    os.environ['PORT'] = '3000'
    os.environ['FEATURE_FLAGS'] = '{"new_ui": true, "analytics": false}'
    
    # Load the configuration
    config = EnvConfigLoader.load_config(config_schema)
    
    # Print results
    print("Loaded Configuration:")
    for key, value in config.items():
        print(f"  {key}: {value} ({type(value).__name__})")

What This Code Does

This snippet provides a robust configuration loader that reads environment variables and automatically converts them to appropriate Python data types. It’s particularly useful for applications following the 12-factor app methodology, where configuration is stored in environment variables.

The EnvConfigLoader class offers several key features:

  1. Type Casting: Automatically converts string values from environment variables to Python data types including:
    • Strings (str)
    • Integers (int)
    • Floats (float)
    • Booleans (bool)
    • Lists (list) - from JSON arrays or comma-separated strings
    • JSON objects (json)
  2. Schema Validation: Defines expected configuration variables with type information, default values, and required/optional status.

  3. Error Handling: Provides clear error messages for missing required variables or parsing failures.

  4. Flexible List Parsing: Supports both JSON-formatted lists and comma-separated values for list-type configuration.

Why This Is Useful

Environment variables are the standard way to configure applications in containerized environments, cloud platforms, and production deployments. However, environment variables are always strings, which means developers need to manually convert them to appropriate data types.

This utility eliminates boilerplate code for configuration management and reduces errors from manual parsing. It’s especially valuable for:

How to Run This Code

  1. Save the code to a file (e.g., config_loader.py)

  2. Set environment variables in your shell:
    export DATABASE_URL="postgresql://user:pass@localhost/mydb"
    export DEBUG="true"
    export PORT="3000"
    export ALLOWED_HOSTS="localhost,127.0.0.1,example.com"
    export FEATURE_FLAGS='{"new_ui": true, "analytics": false}'
    
  3. Run the script:
    python config_loader.py
    
  4. To use in your own project:
    from config_loader import EnvConfigLoader
       
    schema = {
        'DATABASE_URL': {'type': 'str', 'required': True},
        'DEBUG': {'type': 'bool', 'default': False},
        'PORT': {'type': 'int', 'default': 8000}
    }
       
    config = EnvConfigLoader.load_config(schema)
    

The output will show the loaded configuration with properly typed values, demonstrating how string environment variables are automatically converted to their appropriate Python types.