================================================================================ PRACTICAL PYTHON SCRIPTING - CHALLENGE 2 SOLUTION Chapter 2: Configuration & Environment Variables Challenge: Type Conversion and Validation ================================================================================ PROBLEM: Create a script that: 1. Loads a .env file 2. Converts port from string to integer 3. Converts debug flag from string to boolean 4. Validates that required variables exist SOLUTION: ================================================================================ from dotenv import load_dotenv import os import sys from pathlib import Path # Load the .env file env_file = Path(__file__).parent / ".env" load_dotenv(env_file) print("=" * 60) print("CONFIGURATION VALIDATION") print("=" * 60 + "\n") # Step 1: Define required variables REQUIRED_VARS = ['DB_HOST', 'DB_USER', 'DB_PASS', 'DB_NAME'] OPTIONAL_VARS = ['DB_PORT', 'DB_CHARSET', 'DEBUG', 'LOG_LEVEL'] # Step 2: Check required variables exist print("šŸ” Checking required variables...\n") missing_vars = [] for var in REQUIRED_VARS: if var in os.environ: print(f" āœ… {var}: Found") else: print(f" āŒ {var}: MISSING!") missing_vars.append(var) if missing_vars: print(f"\nāŒ ERROR: Missing required variables: {', '.join(missing_vars)}") sys.exit(1) print("\nāœ… All required variables found!\n") # Step 3: Type conversion and validation print("šŸ”„ Converting and validating types...\n") # String (no conversion needed) db_host = os.environ.get('DB_HOST') print(f" DB_HOST: '{db_host}' (string)") db_user = os.environ.get('DB_USER') print(f" DB_USER: '{db_user}' (string)") db_pass = os.environ.get('DB_PASS') print(f" DB_PASS: '{'*' * len(db_pass)}' (string, hidden)") db_name = os.environ.get('DB_NAME') print(f" DB_NAME: '{db_name}' (string)") # Integer conversion (port) try: db_port = int(os.environ.get('DB_PORT', '3306')) print(f" DB_PORT: {db_port} (integer)") except ValueError: print(f" āŒ DB_PORT: Invalid! Must be a number (got '{os.environ.get('DB_PORT')}')") sys.exit(1) # String (charset) db_charset = os.environ.get('DB_CHARSET', 'utf8mb4') print(f" DB_CHARSET: '{db_charset}' (string)") # Boolean conversion (debug mode) debug_str = os.environ.get('DEBUG', 'false').lower() debug = debug_str in ('true', '1', 'yes', 'on') print(f" DEBUG: {debug} (boolean, converted from '{debug_str}')") # String (log level) log_level = os.environ.get('LOG_LEVEL', 'info').upper() print(f" LOG_LEVEL: '{log_level}' (string)") print("\nāœ… All conversions successful!\n") # Step 4: Display final configuration summary print("=" * 60) print("FINAL CONFIGURATION") print("=" * 60 + "\n") print("Database Configuration:") print(f" Host: {db_host}:{db_port}") print(f" Database: {db_name}") print(f" User: {db_user}") print(f" Charset: {db_charset}") print("\nApplication Configuration:") print(f" Debug Mode: {debug}") print(f" Log Level: {log_level}") if debug: print("\nāš ļø DEBUG MODE IS ENABLED - Do not use in production!") print("\n" + "=" * 60) print("āœ… Configuration validated and ready!") print("=" * 60) ================================================================================ HOW IT WORKS: ================================================================================ 1. LOAD CONFIGURATION load_dotenv(env_file) - Load all variables from .env into os.environ 2. CHECK REQUIRED VARIABLES for var in REQUIRED_VARS: if var in os.environ: # Check if variable exists - Loop through required variables - Check if each one exists in os.environ - Store missing ones for error reporting - Exit if any are missing 3. STRING CONVERSION (NO CONVERSION NEEDED) db_host = os.environ.get('DB_HOST') - Strings are the default type - No conversion necessary - .get() returns None if not found 4. INTEGER CONVERSION db_port = int(os.environ.get('DB_PORT', '3306')) - int() converts string to integer - Provide a default ('3306') if not found - int() raises ValueError if the string isn't a number - Use try/except to catch conversion errors 5. BOOLEAN CONVERSION debug_str = os.environ.get('DEBUG', 'false').lower() debug = debug_str in ('true', '1', 'yes', 'on') - .lower() converts to lowercase for comparison - Check if the string matches true values - Handles variations: 'true', '1', 'yes', 'on' - Anything else is False 6. ERROR HANDLING WITH sys.exit(1) - Exit immediately if validation fails - sys.exit(1) means "exit with error" - 0 = success, non-zero = error ================================================================================ UNDERSTANDING TYPE CONVERSION: ================================================================================ String → Integer: int("3306") # Returns 3306 int("30.5") # ValueError! (has decimal point) int("abc") # ValueError! (not a number) int("3306", base=10) # Explicit base (base 10 = decimal) String → Float: float("30.5") # Returns 30.5 float("30") # Returns 30.0 float("abc") # ValueError! String → Boolean: Option 1 (Simple): bool("true") # Returns True (any non-empty string is True!) bool("") # Returns False (empty string is False) bool("false") # Returns True (even the string "false"!) Option 2 (Correct - what we use): debug = "true".lower() in ('true', '1', 'yes', 'on') Option 3 (Using function): def to_bool(s): return s.lower() in ('true', '1', 'yes', 'on') debug = to_bool(os.environ.get('DEBUG', 'false')) ================================================================================ VALIDATION PATTERNS: ================================================================================ Pattern 1: Check if variable exists if var in os.environ: # Variable exists else: # Variable doesn't exist Pattern 2: Get with default value = os.environ.get('VAR', 'default_value') # Uses 'default_value' if VAR not found Pattern 3: Type conversion with error handling try: port = int(os.environ.get('DB_PORT', '3306')) except ValueError: print(f"ERROR: DB_PORT must be a number") sys.exit(1) Pattern 4: Validate range port = int(os.environ.get('DB_PORT', '3306')) if port < 1 or port > 65535: print("ERROR: Port must be between 1 and 65535") sys.exit(1) Pattern 5: Validate choices log_level = os.environ.get('LOG_LEVEL', 'info').lower() if log_level not in ('debug', 'info', 'warning', 'error'): print(f"ERROR: LOG_LEVEL must be one of: debug, info, warning, error") sys.exit(1) ================================================================================ TESTING THE SOLUTION: ================================================================================ 1. First, create a .env file with test values: cat > .env << 'EOF' DB_HOST=localhost DB_NAME=testdb DB_USER=testuser DB_PASS=testpass123 DB_PORT=3306 DB_CHARSET=utf8mb4 DEBUG=true LOG_LEVEL=debug EOF 2. Run the script: python config_validator.py Expected output: ============================================================ CONFIGURATION VALIDATION ============================================================ šŸ” Checking required variables... āœ… DB_HOST: Found āœ… DB_USER: Found āœ… DB_PASS: Found āœ… DB_NAME: Found āœ… All required variables found! šŸ”„ Converting and validating types... DB_HOST: 'localhost' (string) DB_USER: 'testuser' (string) DB_PASS: '***********' (string, hidden) DB_NAME: 'testdb' (string) DB_PORT: 3306 (integer) DB_CHARSET: 'utf8mb4' (string) DEBUG: True (boolean, converted from 'true') LOG_LEVEL: 'DEBUG' (string) āœ… All conversions successful! ============================================================ FINAL CONFIGURATION ============================================================ Database Configuration: Host: localhost:3306 Database: testdb User: testuser Charset: utf8mb4 Application Configuration: Debug Mode: True Log Level: DEBUG āš ļø DEBUG MODE IS ENABLED - Do not use in production! ============================================================ āœ… Configuration validated and ready! ============================================================ 3. Test with missing variable: Remove DB_PASS from .env and run again: Output: šŸ” Checking required variables... āœ… DB_HOST: Found āœ… DB_USER: Found āŒ DB_PASS: MISSING! āœ… DB_NAME: Found āŒ ERROR: Missing required variables: DB_PASS 4. Test with invalid port: Change DB_PORT=abc and run: Output: šŸ”„ Converting and validating types... ... āŒ DB_PORT: Invalid! Must be a number (got 'abc') ================================================================================ REAL-WORLD USAGE: ================================================================================ In production code, you'd create a config function: def load_and_validate_config(): from dotenv import load_dotenv import os import sys load_dotenv() config = { 'db_host': os.environ.get('DB_HOST'), 'db_name': os.environ.get('DB_NAME'), 'db_user': os.environ.get('DB_USER'), 'db_pass': os.environ.get('DB_PASS'), 'db_port': int(os.environ.get('DB_PORT', '3306')), 'debug': os.environ.get('DEBUG', 'false').lower() == 'true', } # Validate required fields required = ['db_host', 'db_name', 'db_user', 'db_pass'] missing = [k for k in required if not config[k]] if missing: print(f"Missing config: {missing}") sys.exit(1) return config # In your main code: if __name__ == '__main__': config = load_and_validate_config() db = connect_to_database(config) ================================================================================ KEY TAKEAWAYS: ================================================================================ āœ… Environment variables are always strings āœ… Use int() to convert to integers āœ… Use float() to convert to floats āœ… Use string comparison for booleans (not bool()) āœ… Always use try/except for type conversion āœ… Validate that required variables exist āœ… Use sys.exit(1) to signal errors āœ… Hide sensitive values in output āœ… Warn if debug mode is enabled in production Type Conversion Checklist: - āœ… Strings: no conversion needed - āœ… Integers: use int() - āœ… Floats: use float() - āœ… Booleans: compare to true values - āœ… All conversions: use try/except - āœ… All conversions: provide defaults - āœ… Always validate required variables ================================================================================