Practical Python Scripting
Course 1 · Chapter 2 · Configuration & Environment Variables
⚙️ Configuration & Environment Variables
Every practical Python script needs to work in different environments. Your database might be on localhost during development, but in production it's at a different address. Your API keys are different for testing vs production. This chapter teaches how to manage configuration securely using environment variables and `.env` files.
⚠️ The Problem: Hard-Coded Secrets
Many scripts hard-code sensitive information:
import mysql.connector
db = mysql.connector.connect(
host="localhost",
user="philip",
password="MySecretPassword123",
database="learning_blog"
)
❌ CRITICAL SECURITY ISSUE:
- 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?
A .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
.env
DB_HOST=localhost
DB_NAME=learning_blog
DB_USER=philip
DB_PASS=AsT@1sAd3mon
DB_PORT=3306
DB_CHARSET=utf8mb4
API_KEY=sk-proj-abc123def456
API_URL=https://api.example.com
DEBUG=true
LOG_LEVEL=info
✅ WHAT IT DOES: A `.env` file is a configuration file with 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_HOST and db_host are different
- No spaces around =:
KEY=value (not KEY = 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_dotenv()
host = os.environ['DB_HOST']
user = os.environ['DB_USER']
password = os.environ['DB_PASS']
print(f"Connecting to {host} as {user}")
✅ WHAT IT DOES: 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
env_path = Path("/var/www/.env")
load_dotenv(env_path)
host = os.environ['DB_HOST']
🤔 WHY SPECIFY A PATH: If `.env` is outside the web root (for security), you need to tell load_dotenv() where to find it.
🔍 Accessing Variables
Method 1: Direct Access (Crashes if Missing)
import os
host = os.environ['DB_HOST']
Method 2: Safe Access with .get()
import os
host = os.environ.get('DB_HOST')
host = os.environ.get('DB_HOST', 'localhost')
if host is None:
print("DB_HOST not defined!")
Direct Access
host = os.environ['DB_HOST']
Safe Access
host = os.environ.get('DB_HOST', 'localhost')
🔄 Type Conversion
Environment variables are always strings. If you need a number or boolean, you must convert them.
Converting to Different Types
import os
from dotenv import load_dotenv
load_dotenv()
host = os.environ.get('DB_HOST')
port = int(os.environ.get('DB_PORT', '3306'))
debug = os.environ.get('DEBUG', 'false').lower() == 'true'
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')
debug = get_bool_env('DEBUG', False)
✅ Validating Configuration
Checking Required Variables
from dotenv import load_dotenv
import os
import sys
load_dotenv()
REQUIRED_VARS = ['DB_HOST', 'DB_USER', 'DB_PASS', 'DB_NAME']
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:
.gitignore
.env
🔒 CRITICAL: If you accidentally commit `.env` with passwords, your database is compromised. You must:
- 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
ls -la .env
.env.example Template
Create a template showing what variables are needed (without real values):
.env.example
DB_HOST=localhost
DB_NAME=learning_blog
DB_USER=your_username
DB_PASS=your_password_here
DB_PORT=3306
DEBUG=false
LOG_LEVEL=info
🌍 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_dotenv()
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
❌ MISTAKE 1: Forgetting load_dotenv()
import os
host = os.environ['DB_HOST']
Fix:
from dotenv import load_dotenv
load_dotenv()
host = os.environ['DB_HOST']
❌ MISTAKE 2: Expecting Variables to be Numbers
port = os.environ['DB_PORT']
if port > 3000:
pass
Fix:
port = int(os.environ['DB_PORT'])
if port > 3000:
pass
❌ MISTAKE 3: Committing .env to Git
git add .
git commit -m "Add configuration"
Fix:
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.
→ Solution
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.
→ Solution
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.
→ Solution
🎯 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.