Configuration & Environment Variables

Database Regeneration - Practical 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.