Python
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax and Data Types
-
Section 3: Collections
-
39: Set Operations: Union, Intersection, Difference
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Turtle Graphics and Early Practice Projects
-
Section 7: Working with Files and I/O
-
Section 8: Regular Expressions
-
Section 9: Object-Oriented Python
-
Section 10: Error Handling
-
Section 11: Modules and Packages
-
Section 12: Iterators, Generators, and Functional Tools
-
Section 13: Decorators and Metaprogramming
-
Section 14: Concurrency and Parallelism
-
Section 15: Working with Dates, Times, and Numbers
-
Section 16: Standard Library Deep Dive I: Data Structures
-
Section 17: Standard Library Deep Dive II: System and Introspection
-
Section 18: Standard Library Deep Dive III: Security and Encoding
-
Section 19: Standard Library Deep Dive IV: Text and Data Utilities
-
Section 20: Networking and Web Basics
-
Section 21: Working with Databases
-
Section 22: Testing and Quality
-
Section 23: Advanced Typing
-
Section 24: Context Managers and Resource Handling
-
Section 25: Text, Unicode, and Binary Data
-
Section 26: More Functional and Iteration Tools
-
Section 27: Data Validation and Configuration
-
Section 28: Working with Images and Media
-
Section 29: Property-Based and Documentation Testing
-
Section 30: Packaging and Deployment
-
Section 31: Performance and Internals
-
Section 32: Design Patterns in Python
-
Section 33: GUI Programming
-
Section 34: Security Basics
-
Section 35: Data Structures and Algorithms
-
Section 36: Practical Projects
-
Section 37: Capstone Projects
-
Section 38: Interview and Algorithm Practice
-
Section 39: Writing Idiomatic Python
354: Managing Application Configuration
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:
- Create a
.envfile containing the variablesDB_NAME,DB_USER,DB_PASS, andDB_HOST. - Implement a
Configclass that loads these values usingpython-dotenv. - Update the
connect()function to use theConfigclass instead of the hardcoded strings. - Add a
DB_PORTvariable to yourConfigclass that defaults to5432and is cast to anint.
There are no comments for now.