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.
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.
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}”)
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.
.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”))
.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’ }”)
.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.
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”)
- 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)
- 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.