Step 1: Understanding the Error
# broken_code.py
import requests
API_KEY = "sk-1234567890abcdef" # Hardcoded API key
DATABASE_URL = "postgresql://user:password@localhost/mydb"
def fetch_data():
response = requests.get(
"https://api.example.com/data",
headers={"Authorization": f"Bearer {API_KEY}"}
)
return response.json()
if __name__ == "__main__":
print(fetch_data())
Running this code exposes several critical issues. When you accidentally commit this file to GitHub, your API keys become public. You might encounter these common errors:
$ python broken_code.py
# Later, after rotating keys:
requests.exceptions.HTTPError: 401 Client Error: Unauthorized
The KeyError happens when Python can't find environment variables:
# another_broken_example.py
import os
api_key = os.environ['API_KEY'] # Raises KeyError if not set
# KeyError: 'API_KEY'
Step 2: Identifying the Cause
Environment variable errors stem from three main sources: missing configuration files, incorrect loading sequences, and improper file management. When you see KeyError: 'API_KEY' or NoneType errors, Python couldn't find your environment variables.
# common_mistake.py
from dotenv import load_dotenv
import os
# Wrong: calling load_dotenv() after trying to access variables
api_key = os.getenv('API_KEY') # Returns None
load_dotenv() # Too late!
print(f"API Key: {api_key}") # Output: API Key: None
The dotenv library reads .env files and loads variables into os.environ. Without proper loading, your variables remain undefined. File path issues also cause failures:
# path_error.py
from dotenv import load_dotenv
# This fails if .env isn't in the current directory
load_dotenv() # Returns False when file not found
# Check if dotenv loaded successfully
if not load_dotenv():
print("Warning: .env file not found")
Step 3: Implementing the Solution
Create a .env file in your project root:
$ touch .env
$ echo "API_KEY=sk-your-actual-api-key" >> .env
$ echo "DATABASE_URL=postgresql://user:pass@localhost/db" >> .env
$ echo "DEBUG=True" >> .env
$ echo "PORT=8000" >> .env
Install python-dotenv:
$ pip install python-dotenv
# or with specific version
$ pip install python-dotenv==1.0.0
Here's the corrected implementation:
# secure_config.py
from dotenv import load_dotenv
import os
from pathlib import Path
# Load .env file from the same directory as this script
env_path = Path(__file__).parent / '.env'
load_dotenv(dotenv_path=env_path)
# Safe way to get environment variables with defaults
API_KEY = os.getenv('API_KEY', 'default-key-for-development')
DATABASE_URL = os.getenv('DATABASE_URL', 'sqlite:///local.db')
DEBUG = os.getenv('DEBUG', 'False').lower() == 'true'
PORT = int(os.getenv('PORT', 8000))
# Validate critical variables
if API_KEY == 'default-key-for-development':
print("Warning: Using default API key. Set API_KEY in .env file")
print(f"Configuration loaded: PORT={PORT}, DEBUG={DEBUG}")
For production environments, use this robust loader:
# production_config.py
import os
import sys
from dotenv import load_dotenv
from pathlib import Path
class Config:
def __init__(self):
# Try multiple .env file locations
possible_paths = [
Path.cwd() / '.env', # Current directory
Path.home() / '.env', # Home directory
Path(__file__).parent / '.env', # Script directory
Path(__file__).parent.parent / '.env' # Parent directory
]
env_loaded = False
for env_path in possible_paths:
if env_path.exists():
load_dotenv(dotenv_path=env_path, override=True)
env_loaded = True
print(f"Loaded environment from: {env_path}")
break
if not env_loaded:
print("No .env file found, using system environment variables")
# Load with validation
self.api_key = self._get_required('API_KEY')
self.database_url = self._get_required('DATABASE_URL')
self.secret_key = self._get_required('SECRET_KEY')
# Optional variables with defaults
self.debug = os.getenv('DEBUG', 'False').lower() == 'true'
self.port = int(os.getenv('PORT', 8000))
self.log_level = os.getenv('LOG_LEVEL', 'INFO')
def _get_required(self, key):
"""Get required environment variable or exit"""
value = os.getenv(key)
if value is None:
print(f"Error: Required environment variable '{key}' is not set")
print("Please add it to your .env file or set it in your environment")
sys.exit(1)
return value
# Usage
config = Config()
print(f"API Key loaded: {config.api_key[:10]}...") # Show only first 10 chars
Step 4: Working Code Example
Here's a complete working example with error handling:
# app.py
import os
import sys
from pathlib import Path
from dotenv import load_dotenv
import requests
from typing import Optional
# Custom exception for configuration errors
class ConfigurationError(Exception):
pass
class EnvironmentManager:
"""Manages environment variables with fallback strategies"""
def __init__(self, env_file: str = '.env'):
self.env_file = env_file
self.variables = {}
self._load_environment()
def _load_environment(self):
"""Load environment variables with multiple fallback options"""
# Method 1: Try loading from .env file
env_path = Path(self.env_file)
if env_path.exists():
load_dotenv(dotenv_path=env_path, override=True)
print(f"✓ Loaded variables from {env_path}")
else:
# Method 2: Try .env.local (for local overrides)
local_env = Path('.env.local')
if local_env.exists():
load_dotenv(dotenv_path=local_env, override=True)
print(f"✓ Loaded variables from {local_env}")
else:
print("⚠ No .env file found, using system environment")
def get(self, key: str, default: Optional[str] = None, required: bool = False) -> Optional[str]:
"""Get environment variable with validation"""
value = os.getenv(key, default)
if required and value is None:
raise ConfigurationError(f"Required variable '{key}' not found")
# Cache the value
self.variables[key] = value
return value
def get_int(self, key: str, default: int = 0) -> int:
"""Get environment variable as integer"""
value = self.get(key, str(default))
try:
return int(value)
except ValueError:
print(f"Warning: '{key}' value '{value}' is not a valid integer, using default {default}")
return default
def get_bool(self, key: str, default: bool = False) -> bool:
"""Get environment variable as boolean"""
value = self.get(key, str(default))
return value.lower() in ('true', '1', 'yes', 'on')
# Initialize environment manager
env = EnvironmentManager()
# Load configuration with proper error handling
try:
API_KEY = env.get('API_KEY', required=True)
API_BASE_URL = env.get('API_BASE_URL', 'https://api.example.com')
TIMEOUT = env.get_int('REQUEST_TIMEOUT', 30)
VERIFY_SSL = env.get_bool('VERIFY_SSL', True)
MAX_RETRIES = env.get_int('MAX_RETRIES', 3)
except ConfigurationError as e:
print(f"Configuration Error: {e}")
print("Please create a .env file with the required variables:")
print("API_KEY=your-api-key-here")
sys.exit(1)
class APIClient:
"""Example API client using environment variables"""
def __init__(self):
self.api_key = API_KEY
self.base_url = API_BASE_URL
self.timeout = TIMEOUT
self.verify_ssl = VERIFY_SSL
self.max_retries = MAX_RETRIES
self.session = requests.Session()
self.session.headers.update({
'Authorization': f'Bearer {self.api_key}',
'Content-Type': 'application/json'
})
def make_request(self, endpoint: str, method: str = 'GET', **kwargs):
"""Make API request with retry logic"""
url = f"{self.base_url}/{endpoint}"
for attempt in range(self.max_retries):
try:
response = self.session.request(
method=method,
url=url,
timeout=self.timeout,
verify=self.verify_ssl,
**kwargs
)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
print(f"Request failed (attempt {attempt + 1}/{self.max_retries}): {e}")
if attempt == self.max_retries - 1:
raise
return None
# Example usage
if __name__ == "__main__":
client = APIClient()
try:
# Test the API connection
result = client.make_request('health')
print(f"API Connection successful: {result}")
except Exception as e:
print(f"API Connection failed: {e}")
Create a comprehensive .env template:
# .env.example - Copy this to .env and fill in your values
# API Configuration
API_KEY=your-api-key-here
API_BASE_URL=https://api.example.com
API_VERSION=v1
# Database Configuration
DATABASE_URL=postgresql://user:password@localhost:5432/dbname
DATABASE_POOL_SIZE=20
DATABASE_ECHO=False
# Security Settings
SECRET_KEY=your-secret-key-here
JWT_SECRET=your-jwt-secret
ENCRYPTION_KEY=your-encryption-key
# Application Settings
DEBUG=False
PORT=8000
HOST=0.0.0.0
LOG_LEVEL=INFO
# Feature Flags
ENABLE_CACHE=True
ENABLE_METRICS=True
# External Services
REDIS_URL=redis://localhost:6379/0
ELASTICSEARCH_URL=http://localhost:9200
# Request Configuration
REQUEST_TIMEOUT=30
MAX_RETRIES=3
VERIFY_SSL=True
Step 5: Additional Tips & Related Errors
Handle missing dotenv module errors:
# safe_import.py
try:
from dotenv import load_dotenv
DOTENV_AVAILABLE = True
except ImportError:
DOTENV_AVAILABLE = False
print("python-dotenv not installed. Install with: pip install python-dotenv")
print("Falling back to system environment variables only")
import os
if DOTENV_AVAILABLE:
load_dotenv()
# Continue with environment variable access
api_key = os.getenv('API_KEY')
Fix encoding issues in .env files:
# encoding_fix.py
from dotenv import load_dotenv
# Specify encoding for Windows systems
load_dotenv(encoding='utf-8')
# For files with special characters
load_dotenv('.env', encoding='latin-1')
Prevent accidental commits with .gitignore:
# Create .gitignore if it doesn't exist
$ touch .gitignore
# Add environment files
$ echo ".env" >> .gitignore
$ echo ".env.local" >> .gitignore
$ echo ".env.*.local" >> .gitignore
$ echo "*.env" >> .gitignore
# Verify files are ignored
$ git status --ignored
Debug environment variable loading:
# debug_env.py
from dotenv import load_dotenv, find_dotenv, dotenv_values
import os
# Find .env file location
dotenv_path = find_dotenv()
print(f"Found .env at: {dotenv_path}")
# Load and display all variables (for debugging only!)
env_vars = dotenv_values(dotenv_path)
print("Loaded variables:")
for key, value in env_vars.items():
# Mask sensitive values
if 'KEY' in key or 'SECRET' in key or 'PASSWORD' in key:
masked_value = value[:4] + '*' * (len(value) - 4) if value else 'None'
print(f" {key} = {masked_value}")
else:
print(f" {key} = {value}")
# Check if specific variable exists
if 'API_KEY' in os.environ:
print("✓ API_KEY is set")
else:
print("✗ API_KEY is not set")
Handle different environments:
# multi_env.py
import os
from dotenv import load_dotenv
# Determine environment
ENV = os.getenv('ENV', 'development')
# Load environment-specific file
env_files = {
'development': '.env.development',
'testing': '.env.testing',
'staging': '.env.staging',
'production': '.env.production'
}
env_file = env_files.get(ENV, '.env')
load_dotenv(env_file)
print(f"Running in {ENV} mode with {env_file}")
# Environment-specific configuration
if ENV == 'production':
# Strict validation for production
required_vars = ['API_KEY', 'DATABASE_URL', 'SECRET_KEY']
missing = [var for var in required_vars if not os.getenv(var)]
if missing:
raise EnvironmentError(f"Missing required variables: {missing}")
Common error fixes reference:
# error_reference.py
"""
Common Environment Variable Errors and Solutions:
1. KeyError: 'VARIABLE_NAME'
Solution: Use os.getenv('VARIABLE_NAME') instead of os.environ['VARIABLE_NAME']
2. ImportError: No module named 'dotenv'
Solution: pip install python-dotenv
3. load_dotenv() returns False
Solution: Check .env file exists and path is correct
4. Variables not updating after change
Solution: Use load_dotenv(override=True) or restart application
5. Special characters in values causing issues
Solution: Quote values in .env file: API_KEY="abc$123"
6. Windows path issues
Solution: Use forward slashes or raw strings: r"C:\path\to\file"
"""
Setting environment variables without files:
# macOS/Linux terminal
$ export API_KEY="your-key-here"
$ export DATABASE_URL="postgresql://localhost/db"
$ python app.py
# Single command
$ API_KEY="your-key" DATABASE_URL="postgresql://localhost/db" python app.py
# Windows Command Prompt
> set API_KEY=your-key-here
> set DATABASE_URL=postgresql://localhost/db
> python app.py
# Windows PowerShell
> $env:API_KEY="your-key-here"
> $env:DATABASE_URL="postgresql://localhost/db"
> python app.py
Validate and sanitize environment variables:
# validation.py
import re
import os
from urllib.parse import urlparse
def validate_api_key(key: str) -> bool:
"""Validate API key format"""
# Example: API key should be 32 characters alphanumeric
pattern = r'^[A-Za-z0-9]{32,}$'
return bool(re.match(pattern, key))
def validate_database_url(url: str) -> bool:
"""Validate database URL format"""
try:
result = urlparse(url)
return all([result.scheme, result.netloc])
except:
return False
def validate_email(email: str) -> bool:
"""Validate email format"""
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
return bool(re.match(pattern, email))
# Apply validation
api_key = os.getenv('API_KEY', '')
if not validate_api_key(api_key):
print("Error: Invalid API key format")
database_url = os.getenv('DATABASE_URL', '')
if not validate_database_url(database_url):
print("Error: Invalid database URL format")
Environment variables provide essential configuration management for Python applications. Proper implementation prevents security breaches and configuration errors. Remember to never commit .env files, always validate input, and use appropriate fallback values for missing variables.