Configuration & Environment Variables
⚙️ Configuration & Environment Variables
⚠️ The Problem: Hard-Coded Secrets
Many scripts hard-code sensitive information:
import mysql.connector # ❌ NEVER DO THIS! db = mysql.connector.connect( host="localhost", user="philip", password="MySecretPassword123", # ← BAD! database="learning_blog" )
- Password is visible in the source code
- If you commit to Git, it's in version control forever
- Anyone who reads the code gets your database password
- If the repo goes public, attackers have access
The solution: Move secrets to a `.env` file that's never committed to Git.
📝 What is a .env File?
.env file is a simple text file that stores configuration variables. It's kept on the server (not in Git), and your script reads it at startup.
A Real .env File Example
KEY=VALUE pairs. Each line is one setting. Comments start with #.
Format Rules
- One setting per line:
KEY=VALUE - No quotes needed:
DB_HOST=localhost(not"localhost") - Comments with #:
# This is a comment - Empty lines ignored: Blank lines are skipped
- Case sensitive:
DB_HOSTanddb_hostare different - No spaces around =:
KEY=value(notKEY = value)
📦 Installing python-dotenv
To load `.env` files in Python, you need the python-dotenv package:
pip install python-dotenv
Add it to your requirements.txt:
mysql-connector-python==8.2.0 python-dotenv==1.0.0
🔓 Loading Environment Variables
The load_dotenv() Function
from dotenv import load_dotenv import os # Load .env file from current directory (or parents) load_dotenv() # Now read variables using os.environ host = os.environ['DB_HOST'] user = os.environ['DB_USER'] password = os.environ['DB_PASS'] print(f"Connecting to {host} as {user}")
load_dotenv() reads the `.env` file and loads all variables into os.environ (a dictionary of environment variables).
With Explicit Path
If `.env` is in a specific location:
from dotenv import load_dotenv from pathlib import Path import os # Specify exact path to .env env_path = Path("/var/www/.env") load_dotenv(env_path) host = os.environ['DB_HOST']
load_dotenv() where to find it.
🔍 Accessing Variables
Method 1: Direct Access (Crashes if Missing)
import os # This will crash if DB_HOST is not defined host = os.environ['DB_HOST'] # KeyError: 'DB_HOST' if variable doesn't exist
Method 2: Safe Access with .get()
import os # Returns the value, or None if not defined host = os.environ.get('DB_HOST') # With a default value if not defined host = os.environ.get('DB_HOST', 'localhost') # Check if it exists if host is None: print("DB_HOST not defined!")
host = os.environ['DB_HOST'] # Crashes if missing # Good for required settings
host = os.environ.get('DB_HOST', 'localhost') # Returns default if missing # Good for optional settings
🔄 Type Conversion
Converting to Different Types
import os from dotenv import load_dotenv load_dotenv() # String (default, no conversion needed) host = os.environ.get('DB_HOST') # "localhost" # Integer (port number) port = int(os.environ.get('DB_PORT', '3306')) # String "3306" → Integer 3306 # Boolean (debug mode) debug = os.environ.get('DEBUG', 'false').lower() == 'true' # String "true" → Boolean True # Float (timeout in seconds) timeout = float(os.environ.get('TIMEOUT', '30.5')) print(f"Host: {host} (type: {type(host).__name__})") print(f"Port: {port} (type: {type(port).__name__})") print(f"Debug: {debug} (type: {type(debug).__name__})")
Helper Function for Booleans
def get_bool_env(key: str, default: bool = False) -> bool: """Convert environment variable to boolean.""" value = os.environ.get(key, str(default)) return value.lower() in ('true', '1', 'yes', 'on') # Usage: debug = get_bool_env('DEBUG', False)
✅ Validating Configuration
Checking Required Variables
from dotenv import load_dotenv import os import sys load_dotenv() # List of required variables REQUIRED_VARS = ['DB_HOST', 'DB_USER', 'DB_PASS', 'DB_NAME'] # Check each one missing = [] for var in REQUIRED_VARS: if var not in os.environ: missing.append(var) if missing: print("❌ Missing required variables:") for var in missing: print(f" - {var}") sys.exit(1) print("✅ All required variables found!")
🔒 Security Best Practices
Never Commit .env to Git
Add `.env` to your `.gitignore` file:
- Change all passwords immediately
- Remove the commit from Git history (or assume secrets are leaked)
- Check if anyone accessed your database
File Permissions
On Linux/Mac, restrict `.env` to be readable only by the owner:
chmod 600 .env # Now only the owner can read it # Verify: ls -la .env # Should show: -rw------- (owner read/write only)
.env.example Template
Create a template showing what variables are needed (without real values):
🌍 Real-World Example: db_utils.py
Here's how the actual database utility script uses environment variables:
from dotenv import load_dotenv import os import mysql.connector # Load .env file load_dotenv() # Get database credentials from environment DB_CONFIG = { 'host': os.environ.get('DB_HOST'), 'user': os.environ.get('DB_USER'), 'password': os.environ.get('DB_PASS'), 'database': os.environ.get('DB_NAME'), 'port': int(os.environ.get('DB_PORT', '3306')), 'charset': os.environ.get('DB_CHARSET', 'utf8mb4') } class DatabaseConnection: def __init__(self): self.connection = None def connect(self): try: self.connection = mysql.connector.connect(**DB_CONFIG) return True except Exception as e: print(f"❌ Connection failed: {e}") return False
⚠️ Common Mistakes
import os host = os.environ['DB_HOST'] # KeyError: 'DB_HOST' not found! # (You never called load_dotenv() to load the .env file)Fix:
from dotenv import load_dotenv load_dotenv() # ← Add this! host = os.environ['DB_HOST']
port = os.environ['DB_PORT'] # This is a STRING "3306" if port > 3000: # ❌ TypeError: '>' not supported between str and int passFix:
port = int(os.environ['DB_PORT']) # ← Convert to integer if port > 3000: # ✅ Now it works pass
git add .
git commit -m "Add configuration"
# Oops! You just committed DB_PASS with your real password!
Fix:
# Add to .gitignore BEFORE committing anything:
echo ".env" >> .gitignore
git add .gitignore
git commit -m "Add .gitignore"
💻 Coding Challenges
Challenge 1: Load and Display Configuration
Create a script that:
- Creates a `.env` file with database settings
- Loads it with `load_dotenv()`
- Displays all settings (except the password)
Goal: Practice creating and loading `.env` files.
Challenge 2: Type Conversion and Validation
Create a script that:
- Loads a `.env` file
- Converts port from string to integer
- Converts debug flag from string to boolean
- Validates that required variables exist
Goal: Practice type conversion and validation.
Challenge 3: Secure Configuration Management
Create a script that:
- Loads configuration with fallback defaults
- Validates all required variables
- Reports configuration status (what's set, what uses defaults)
- Shows security warnings if DEBUG is true in production
Goal: Practice building robust configuration systems.
🎯 What's Next
Now that you can load configuration securely, the next chapter covers Database Connections — how to use this configuration to connect to MySQL/MariaDB and execute queries.