Skip to Content
Course content

354: Managing Application Configuration

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I'm currently working on a small weather-tracking service that hits an external API. Right now, it's working, but it's "working" in the worst way possible. Let's look at my current setup.

import requests

# I'm just testing this locally, so I'll put the key here for now
API_KEY = "abc123xyz789_secret_key"
BASE_URL = "https://api.weatherstack.com/current"
CACHE_TIMEOUT = 3600

def get_weather(city):
    response = requests.get(f"{BASE_URL}?access_key={API_KEY}&query={city}")
    return response.json()

print(get_weather("New York"))

The "Oh No" Moment

It works, but I just realized I'm about to push this to a shared repository. If I do that, my API key is now public knowledge. That's a disaster waiting to happen. Even if the project is private, hardcoding settings means that if I want to change the CACHE_TIMEOUT from one hour to ten minutes, I have to edit the source code and redeploy the whole thing.

I need a way to separate my logic from my configuration. The industry standard here is to use environment variables. I'll start by trying to pull the key directly from the system using the os module.

import os
import requests

# Now I'm pulling from the environment
API_KEY = os.getenv("WEATHER_API_KEY")
BASE_URL = "https://api.weatherstack.com/current"

def get_weather(city):
    if not API_KEY:
        raise ValueError("Missing WEATHER_API_KEY environment variable!")
    response = requests.get(f"{BASE_URL}?access_key={API_KEY}&query={city}")
    return response.json()

Now, if I run this, it crashes immediately with my ValueError because I haven't actually set that variable in my shell. I could run export WEATHER_API_KEY=... in my terminal, but that's a pain to do every time I open a new window. I want something that feels like a local config file but doesn't get checked into Git.

Managing Local Secrets with .env

I'm going to use a .env file. It's basically a plain text file where I can list my secrets. I'll create a file named .env in my root directory:

WEATHER_API_KEY=abc123xyz789_secret_key
CACHE_TIMEOUT=3600
DEBUG=True

Crucially, I'm adding .env to my .gitignore file immediately. Now I need Python to actually read this. I'll use the python-dotenv library because it's the most straightforward way to bridge the gap between a file and os.getenv().

from dotenv import load_dotenv
import os

load_dotenv() # This looks for a .env file and loads it into the environment

API_KEY = os.getenv("WEATHER_API_KEY")
print(f"Key loaded: {API_KEY[:4]}...") 

This is much better. My secrets are out of the code, and I can have different .env files for my laptop, my staging server, and my production server.

The String Trap

Here is where I usually trip up. I tried to use my CACHE_TIMEOUT from the .env file, but look what happens:

CACHE_TIMEOUT = os.getenv("CACHE_TIMEOUT")
print(type(CACHE_TIMEOUT)) 
# Output: <class 'str'>

# This will crash:
# total_time = CACHE_TIMEOUT + 60 

Everything coming out of os.getenv() is a string. Even if I wrote 3600 or True in the file, Python sees them as "3600" and "True". This is a common source of bugs where if os.getenv("DEBUG"): always evaluates to True because a non-empty string is truthy, even if the string is "False".

Building a Robust Config Layer

Instead of calling os.getenv() all over my application, I'll centralize the configuration into a class. This allows me to handle type casting in one place and provide sensible defaults if a variable is missing.

import os
from dotenv import load_dotenv

load_dotenv()

class Config:
    # Strings are easy
    API_KEY = os.getenv("WEATHER_API_KEY")
    BASE_URL = os.getenv("BASE_URL", "https://api.weatherstack.com/current")
    
    # Integers need explicit casting
    CACHE_TIMEOUT = int(os.getenv("CACHE_TIMEOUT", 3600))
    
    # Booleans are tricky; "False" is a truthy string!
    DEBUG = os.getenv("DEBUG", "False").lower() == "true"

# Now the rest of the app uses the Config object
if Config.DEBUG:
    print("Running in debug mode...")

print(f"Timeout is {Config.CACHE_TIMEOUT} seconds.")

By doing this, I've created a single "source of truth." If the API provider changes their base URL or I need to add a new timeout setting, I change it in the .env file or the Config class, and the rest of the application just works without me having to hunt down every instance of a string conversion.




📋 Practical Task

Exercise: Secure the Database Connector

You are taking over a project where the database credentials are currently hardcoded in a script. Your task is to migrate these to a configuration pattern to prevent security leaks.

Current Code:

# db_connector.py
import psycopg2

def connect():
    # DANGER: Hardcoded credentials!
    conn = psycopg2.connect(
        dbname="prod_db",
        user="admin_user",
        password="super_secret_password_123",
        host="10.0.0.5"
    )
    return conn

Your Goal:

  1. Create a .env file containing the variables DB_NAME, DB_USER, DB_PASS, and DB_HOST.
  2. Implement a Config class that loads these values using python-dotenv.
  3. Update the connect() function to use the Config class instead of the hardcoded strings.
  4. Add a DB_PORT variable to your Config class that defaults to 5432 and is cast to an int.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.