Configuration & Environment Variables

Practical Python Scripting — Configuration & Environment Variables
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 # ❌ NEVER DO THIS! db = mysql.connector.connect( host="localhost", user="philip", password="MySecretPassword123", # ← BAD! 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 # Database Configuration DB_HOST=localhost DB_NAME=learning_blog DB_USER=philip DB_PASS=AsT@1sAd3mon DB_PORT=3306 DB_CHARSET=utf8mb4 # API Keys API_KEY=sk-proj-abc123def456 API_URL=https://api.example.com # Application Settings 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 .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}")
✅ 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 # Specify exact path to .env 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 # 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!")
Direct Access
host = os.environ['DB_HOST'] # Crashes if missing # Good for required settings
Safe Access
host = os.environ.get('DB_HOST', 'localhost') # Returns default if missing # Good for optional settings

🔄 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() # 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:

.gitignore # Environment variables with secrets .env # But keep an example file (without secrets) # .env.example
🔒 CRITICAL: If you accidentally commit `.env` with passwords, your database is compromised. You must:
  1. Change all passwords immediately
  2. Remove the commit from Git history (or assume secrets are leaked)
  3. 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):

.env.example # Copy this file to .env and fill in real values # DO NOT commit .env to git! 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 .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

❌ MISTAKE 1: Forgetting load_dotenv()
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']
❌ MISTAKE 2: Expecting Variables to be Numbers
port = os.environ['DB_PORT'] # This is a STRING "3306" if port > 3000: # ❌ TypeError: '>' not supported between str and int pass
Fix:
port = int(os.environ['DB_PORT']) # ← Convert to integer if port > 3000: # ✅ Now it works pass
❌ MISTAKE 3: Committing .env to Git
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:

  1. Creates a `.env` file with database settings
  2. Loads it with `load_dotenv()`
  3. Displays all settings (except the password)

Goal: Practice creating and loading `.env` files.

→ Solution

Challenge 2: Type Conversion and Validation

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

Goal: Practice type conversion and validation.

→ Solution

Challenge 3: Secure Configuration Management

Create a script that:

  1. Loads configuration with fallback defaults
  2. Validates all required variables
  3. Reports configuration status (what's set, what uses defaults)
  4. 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.