0%

Secrets & Settings: Environment Variables in Python

Keep your API keys and passwords out of your code. Manage configuration across development, testing, and production. The professional way to handle secrets.

Your code works. But your API key is hard-coded. Your database password is in plain text. You change configuration values by editing the code. This works on your laptop. But what happens when you share your code? Push to GitHub? Deploy to a server? Work with a team?
Hard-coded secrets are a security risk. They leak. They cause merge conflicts. They make deployment impossible.
Environment variables are the solution. They keep configuration outside your code. They change per environment (development, testing, production). They never get committed to version control.
This tutorial teaches you to use environment variables with Python. You will learn to read environment variables, use the python-dotenv library, manage different environments, and follow security best practices.

🕯️ Magic Note

The Twelve-Factor App methodology, widely used in modern software development, recommends storing configuration in environment variables. This makes applications portable, secure, and easy to deploy across different environments.

What Are Environment Variables?
Environment variables are key-value pairs stored outside your program but accessible to it.

Bash (Setting environment variables)

# Linux / macOS (temporary)

export DATABASE_URL=”postgresql://localhost/mydb”

export API_KEY=”sk-1234567890″

python my_script.py

# Windows (Command Prompt)

set DATABASE_URL=postgresql://localhost/mydb

python my_script.py

# Windows (PowerShell)

$env:DATABASE_URL=”postgresql://localhost/mydb”

python my_script.py

Python (Reading environment variables)

import os

# Read environment variable (returns None if not set)

db_url = os.environ.get(“DATABASE_URL”)

api_key = os.environ.get(“API_KEY”)

# Read with default value

port = os.environ.get(“PORT”, 5000)

debug = os.environ.get(“DEBUG”, “False”) == “True”

# Raise error if missing (for required variables)

secret_key = os.environ[“SECRET_KEY”] # KeyError if not set

print(f”Database: {db_url}”)

print(f”Port: {port}”)

🕯️ Magic Note

Environment variables are inherited by child processes. If you set a variable in your shell and then run a Python script, the script can access it. This is how cloud platforms (Heroku, Render, AWS) pass configuration to your application.

The .env File: dotenv Library
For local development, managing shell variables is tedious. The python-dotenv library reads variables from a .env file.

Bash

# Install python-dotenv

pip install python-dotenv

.env (create this file in your project root)

# This file contains secrets – NEVER commit it to Git!

DATABASE_URL=postgresql://localhost:5432/myapp

API_KEY=sk_live_abc123def456

SECRET_KEY=your-secret-key-here

DEBUG=true

PORT=8080

MAX_CONNECTIONS=100

Python (Loading .env file)

import os

from dotenv import load_dotenv

# Load .env file (looks for .env in current directory)

load_dotenv()

# Now environment variables are available

db_url = os.environ.get(“DATABASE_URL”)

api_key = os.environ.get(“API_KEY”)

debug = os.environ.get(“DEBUG”, “False”).lower() == “true”

print(f”Connecting to: {db_url}”)

print(f”Debug mode: {debug}”)

💡 Create a .env.example file with placeholder values and commit it to Git. This shows other developers what variables they need. The actual .env file should be in .gitignore.
Multiple Environments: .env.dev, .env.prod
Use different environment files for development, testing, and production.

Project Structure

my_project/

├── .env.dev # Development environment

├── .env.test # Testing environment

├── .env.prod # Production (never committed)

├── .env.example # Template (commit this)

├── .gitignore

└── app.py

.env.example (commit to Git)

# Copy this file to .env.dev, .env.test, or .env.prod

# and fill in your values

DATABASE_URL=postgresql://localhost:5432/myapp

API_KEY=your-api-key-here

SECRET_KEY=change-this-to-a-secret

DEBUG=false

PORT=5000

Python (Loading specific environment file)

import os

from dotenv import load_dotenv

# Get environment name from shell variable

env = os.environ.get(“PYTHON_ENV”, “dev”)

# Load appropriate .env file

if env == “prod”:

load_dotenv(“.env.prod”)

elif env == “test”:

load_dotenv(“.env.test”)

else:

load_dotenv(“.env.dev”)

# Use the variables

db_url = os.environ.get(“DATABASE_URL”)

print(f”Running in {env} mode”)

print(f”Database: {db_url}”)

Bash (Run with different environments)

# Development

PYTHON_ENV=dev python app.py

# Test

PYTHON_ENV=test pytest

# Production (in deployment scripts)

PYTHON_ENV=prod gunicorn app:app

🕯️ Magic Note

Never commit real secrets to Git. Use .env.example as a template. The actual .env files should be in .gitignore. This is a security best practice followed by all professional projects.

Practical Example: Flask App Configuration
Using environment variables to configure a Flask application.

.env.dev

FLASK_APP=app.py

FLASK_ENV=development

SECRET_KEY=dev-secret-key-not-for-production

DATABASE_URL=sqlite:///dev.db

DEBUG=true

API_BASE_URL=https://api.dev.example.com

.env.prod (never committed)

FLASK_APP=app.py

FLASK_ENV=production

SECRET_KEY=3f7a8b2c9d1e4f5a6b7c8d9e0f1a2b3c

DATABASE_URL=postgresql://user:pass@prod-db:5432/myapp

DEBUG=false

API_BASE_URL=https://api.example.com

REDIS_URL=redis://cache:6379/0

Python (config.py)

import os

from dotenv import load_dotenv

# Load environment-specific .env file

env = os.environ.get(“FLASK_ENV”, “development”)

env_file = f”.env.{env}” if env != “development” else “.env.dev”

load_dotenv(env_file)

class Config:

“””Base configuration.”””

SECRET_KEY = os.environ.get(“SECRET_KEY”, “dev-key”)

DEBUG = os.environ.get(“DEBUG”, “False”).lower() == “true”

# Database

DATABASE_URL = os.environ.get(“DATABASE_URL”, “sqlite:///app.db”)

# API

API_BASE_URL = os.environ.get(“API_BASE_URL”, “https://api.example.com”)

@classmethod

def is_production(cls):

return os.environ.get(“FLASK_ENV”) == “production”

class DevelopmentConfig(Config):

DEBUG = True

class ProductionConfig(Config):

DEBUG = False

# Select config based on environment

config_map = {

“development”: DevelopmentConfig,

“production”: ProductionConfig

}

config = config_map.get(os.environ.get(“FLASK_ENV”, “development”))

Practical Example: API Client with Keys
A weather API client that reads its API key from environment variables.

.env

OPENWEATHER_API_KEY=abc123def456ghi789

DEFAULT_CITY=Tehran

UNITS=metric

Python (weather.py)

import os

import requests

from dotenv import load_dotenv

# Load environment variables

load_dotenv()

# Required variables (will crash if missing)

API_KEY = os.environ[“OPENWEATHER_API_KEY”]

# Optional variables with defaults

DEFAULT_CITY = os.environ.get(“DEFAULT_CITY”, “London”)

UNITS = os.environ.get(“UNITS”, “metric”)

def get_weather(city: str = None):

city = city or DEFAULT_CITY

url = “https://api.openweathermap.org/data/2.5/weather”

params = {

“q”: city,

“appid”: API_KEY,

“units”: UNITS

}

response = requests.get(url, params=params)

response.raise_for_status()

return response.json()

if __name__ == “__main__”:

weather = get_weather()

print(f”Weather in {DEFAULT_CITY}: {weather[‘weather’][0][‘description’]}”)

print(f”Temperature: {weather[‘main’][‘temp’]}°{ ‘C’ if UNITS == ‘metric’ else ‘F’ }”)

Security: .gitignore Best Practices
Never commit sensitive files to version control.

.gitignore

# Environment files with real secrets

.env

.env.dev

.env.prod

.env.local

.env.*.local

# But commit the example file

# .env.example (commit this!)

# Also ignore other sensitive files

*.key

*.pem

*.crt

secrets.json

config.local.py

.env.example (commit this)

# Copy this file to .env and fill in your values

# Never commit .env to version control

# Database

DATABASE_URL=postgresql://localhost:5432/myapp

# API Keys (use placeholder values)

OPENWEATHER_API_KEY=your-api-key-here

STRIPE_SECRET_KEY=sk_test_xxxxx

# Application Settings

SECRET_KEY=change-this-to-a-random-secret

DEBUG=true

PORT=5000

🕯️ Magic Note

If you accidentally commit a secret to Git, assume it is compromised. Rotate the key immediately. Even if you delete the commit, the secret remains in Git history. Use tools like git-secrets or truffleHog to prevent leaks.

Environment Variables in Testing
Use different environment variables for tests to avoid affecting development data.

Python (tests/conftest.py)

import os

import pytest

from dotenv import load_dotenv

@pytest.fixture(scope=”session”, autouse=True)

def test_environment():

“””Load test environment variables.”””

load_dotenv(“.env.test”)

# Override critical variables for tests

os.environ[“DATABASE_URL”] = “sqlite:///:memory:”

os.environ[“DEBUG”] = “True”

os.environ[“API_KEY”] = “test-key”

yield

# Cleanup if needed

def test_database_connection():

db_url = os.environ.get(“DATABASE_URL”)

assert db_url == “sqlite:///:memory:”

print(“Test database configured correctly”)

Common Mistakes with Environment Variables
  • Committing .env to Git (security risk)
  • Forgetting to restart the application after changing variables
  • Using os.environ[“KEY”] without checking existence (raises KeyError)
  • Not loading .env in Jupyter notebooks (must call load_dotenv() explicitly)
  • Hard-coding fallback values for sensitive keys (should fail loudly)
  • Storing structured data (JSON, lists) in environment variables (use simple strings)
Check Your Understanding
  • How do you read an environment variable in Python?
  • What is the difference between os.environ.get(“KEY”) and os.environ[“KEY”]?
  • Why should you never commit the .env file to Git?
  • What is the purpose of the .env.example file?
  • How do you load variables from a .env file using python-dotenv?
  • How would you use different environment files for development and production?

⚡ Whisper

Secrets do not belong in code. Passwords, API keys, tokens—they should live outside. Environment variables are the boundary. The code asks: “What is the database URL?” The environment answers. The code asks: “What is the API key?” The deployment provides. This separation is security. This separation is portability. Your code runs on any machine. Your secrets stay where they belong. Use .env for development. Use platform variables for production. Never commit secrets. Never hard-code keys. The world will see your code. The secrets must stay hidden. Protect them. Your users depend on it.

Related posts