Skip to main content

Environment Variables

What You'll Learn​

Why environment variables are the standard way to configure Python programs, how to read them safely, and how to manage them across environments.

Why Environment Variables?​

Never hardcode config in your code:

# ❌ Hardcoded — commits secrets to git, breaks in other environments
DB_PASSWORD = "mysecret123"
API_KEY = "sk-abc123xyz"
SERVER = "192.168.1.100"

Use environment variables instead:

# ✅ Read from environment — safe, portable, configurable
import os
DB_PASSWORD = os.environ.get("DB_PASSWORD")
API_KEY = os.environ.get("API_KEY")
SERVER = os.environ.get("SERVER", "localhost")

This way:

  • Secrets never appear in your codebase
  • Each environment (dev, staging, prod) uses its own values
  • Docker, CI/CD, and Kubernetes can inject config at runtime

Reading Environment Variables​

import os

# Get — returns None if not set
value = os.environ.get("MY_VAR")

# Get with default
port = os.environ.get("PORT", "8080") # still a string!
port = int(os.environ.get("PORT", "8080")) # convert as needed

# Direct access — raises KeyError if not set
value = os.environ["REQUIRED_VAR"]

# Check if set
if "API_KEY" in os.environ:
key = os.environ["API_KEY"]

# All env vars as a dict
all_vars = dict(os.environ)

Type Conversion Patterns​

Environment variables are always strings. Convert them correctly:

import os

# Integer
port = int(os.environ.get("PORT", "8080"))
workers = int(os.environ.get("WORKERS", "4"))

# Float
timeout = float(os.environ.get("TIMEOUT", "30.0"))

# Boolean — several common conventions
debug = os.environ.get("DEBUG", "false").lower() in ("1", "true", "yes", "on")

# Path
from pathlib import Path
data_dir = Path(os.environ.get("DATA_DIR", "/var/data"))

# List (comma-separated)
allowed = os.environ.get("ALLOWED_HOSTS", "localhost").split(",")
# ["localhost"] or ["myapp.com", "api.myapp.com"]

Fail Fast on Missing Required Variables​

Don't let missing config cause mysterious errors later — check at startup:

import os
import sys

REQUIRED = ["DATABASE_URL", "API_KEY", "SECRET_KEY"]

missing = [var for var in REQUIRED if not os.environ.get(var)]
if missing:
print(f"Error: missing required environment variables: {', '.join(missing)}", file=sys.stderr)
sys.exit(1)

# Now safe to use
db_url = os.environ["DATABASE_URL"]
api_key = os.environ["API_KEY"]

Setting Env Vars in the Shell​

# Set for current session
export PORT=8080
export DEBUG=true
export DATABASE_URL="postgresql://user:pass@localhost/mydb"

# Set just for one command (doesn't affect shell)
PORT=8080 DEBUG=true python3 app.py

# Unset a variable
unset API_KEY

# List all env vars
env | grep MY_APP

.env Files for Local Development​

Create a .env file for local dev (never commit to git):

# .env — local development settings
DATABASE_URL=postgresql://localhost/myapp_dev
API_KEY=test_key_local_only
DEBUG=true
PORT=8080
LOG_LEVEL=DEBUG
SECRET_KEY=dev-secret-not-for-production

Load with python-dotenv:

pip install python-dotenv
from dotenv import load_dotenv
import os

# Load .env into os.environ (skips vars already set in environment)
load_dotenv()

db_url = os.environ.get("DATABASE_URL")
debug = os.environ.get("DEBUG", "false").lower() == "true"

Always add to .gitignore:

.env
.env.local
.env.*.local

Centralize all env var reading in one place:

import os
import sys
from dataclasses import dataclass
from pathlib import Path


@dataclass(frozen=True)
class AppConfig:
# Required — no defaults
database_url: str
secret_key: str

# Optional with defaults
debug: bool = False
port: int = 8080
log_level: str = "INFO"
data_dir: Path = Path("/var/data")

@classmethod
def from_env(cls) -> "AppConfig":
"""Read config from environment variables."""
# Collect missing required vars
missing = []
database_url = os.environ.get("DATABASE_URL")
secret_key = os.environ.get("SECRET_KEY")

if not database_url:
missing.append("DATABASE_URL")
if not secret_key:
missing.append("SECRET_KEY")

if missing:
print(f"Missing env vars: {', '.join(missing)}", file=sys.stderr)
sys.exit(1)

return cls(
database_url=database_url,
secret_key=secret_key,
debug=os.environ.get("DEBUG", "false").lower() == "true",
port=int(os.environ.get("PORT", "8080")),
log_level=os.environ.get("LOG_LEVEL", "INFO").upper(),
data_dir=Path(os.environ.get("DATA_DIR", "/var/data")),
)


# Usage at startup
from dotenv import load_dotenv
load_dotenv()
config = AppConfig.from_env()
print(f"Starting on port {config.port}, debug={config.debug}")

Environment-Specific Files​

Use different .env files per environment:

.env ← local defaults (committed, no secrets)
.env.local ← local overrides (gitignored)
.env.staging ← staging config (gitignored)
.env.production ← never create this! use secrets manager

Load with a specific file:

from dotenv import load_dotenv
load_dotenv(".env.staging")

Secrets in Production​

For production, never use .env files. Use a secrets manager:

PlatformTool
AWSSecrets Manager, Parameter Store
GCPSecret Manager
AzureKey Vault
KubernetesSecrets
Cloudflare WorkersSecrets API
HerokuConfig Vars

These inject secrets as environment variables at runtime — your code reads them the same way.

Common Mistakes​

MistakeConsequenceFix
Committing .env to gitSecret exposureAdd to .gitignore
Hardcoding secrets in codeCan't rotate, exposed in gitUse os.environ.get()
No fail-fast for missing varsMysterious crashes laterCheck at startup
Using os.environ["KEY"] for optional varsKeyError crashUse .get("KEY", default)
Trusting .env in productionSecurity riskUse secrets manager

Quick Reference​

import os

# Read
os.environ.get("KEY") # None if missing
os.environ.get("KEY", "default") # with default
os.environ["KEY"] # KeyError if missing

# Convert
int(os.environ.get("PORT", "8080"))
float(os.environ.get("TIMEOUT", "30.0"))
os.environ.get("DEBUG", "false").lower() == "true"
Path(os.environ.get("DATA_DIR", "/tmp"))
os.environ.get("HOSTS", "a,b").split(",")

# Fail fast
if not os.environ.get("API_KEY"):
sys.exit("API_KEY required")

# .env files
from dotenv import load_dotenv
load_dotenv()

What's Next​

→ Module 6: Errors, Logging, and Observability