Environment variables are named text settings provided to a program by the operating system or the platform that runs it. Python reads them through os.environ, a dictionary-like object, or os.getenv(), which returns a default when a variable is missing:
import os
name = os.getenv("WORKSHOP_NAME", "Canal Workshop")
api_key = os.environ["STOCK_API_KEY"] # KeyError if not setKeeping configuration outside the code lets the same program run on your laptop, a test server and production with different databases, keys and limits, and keeps secrets out of version control. This lesson covers reading values, converting them safely, failing clearly when something required is missing, and handling secrets. The examples configure a bicycle workshop's stock system.
Environment values
Every environment variable is a string. os.environ behaves like a dictionary of them:
import os
os.environ["WORKSHOP_NAME"] # KeyError if missing
os.environ.get("WORKSHOP_NAME", "Canal Workshop")
os.getenv("WORKSHOP_NAME", "Canal Workshop") # same as .get()
"WORKSHOP_NAME" in os.environ # True or FalseUse os.environ["NAME"] when the setting is required and a missing value is an error; use os.getenv("NAME", default) for optional settings.
Variables are set outside Python: export STOCK_BATCH=50 in macOS and Linux shells, $env:STOCK_BATCH = "50" in PowerShell, set STOCK_BATCH=50 in Command Prompt, or through your hosting platform's dashboard. A running program sees only the environment it was started with; changing a variable in another terminal has no effect. Assigning to os.environ inside Python affects the current process and any child processes it starts, which is handy in tests.
Parsing settings
Because values are text, convert them once, at start-up, and validate the result:
import os
def env_int(name, default, low=None, high=None):
raw = os.getenv(name, str(default))
try:
value = int(raw)
except ValueError:
raise ValueError(f"{name} must be a whole number, got {raw!r}") from None
if low is not None and value < low:
raise ValueError(f"{name} must be at least {low}")
if high is not None and value > high:
raise ValueError(f"{name} must be at most {high}")
return value
def env_bool(name, default=False):
raw = os.getenv(name)
if raw is None:
return default
return raw.strip().lower() in ("1", "true", "yes", "on")bool(os.getenv("DEBUG")) is a trap: any non-empty string, including "false" and "0", is truthy. Parse booleans by comparing words, as above. Converting everything up front means the rest of the program works with proper int, bool and Path values, and a misconfiguration stops the program immediately with a clear message rather than failing an hour later.
Required configuration
A setting the program can't run without should produce an actionable error at start-up: say which variable is missing and what it should contain.
import os, sys
missing = [name for name in ("DATABASE_URL", "STOCK_API_KEY") if not os.getenv(name)]
if missing:
sys.exit(f"Missing required settings: {', '.join(missing)}. See README for setup.")Collecting all missing names before exiting saves the person deploying the program from fixing them one restart at a time. Some settings are only required when a feature is enabled: check DATABASE_URL only if USE_DATABASE is true.
Be careful with fallbacks for important settings. A default such as os.getenv("DATABASE_URL", "postgres://localhost/prod") can silently connect a test run to the wrong database. Defaults are fine for harmless values like a display name or batch size; required connections and credentials should fail loudly when missing.
Secret boundaries
API keys, passwords and tokens must not be written in source code, because code is shared, copied and pushed to repositories where history keeps every version. Read them from the environment or a secrets manager instead, and keep them out of output:
token = os.environ["STOCK_API_KEY"]
print(f"Using API key ending ...{token[-4:]}") # never print the full valueDuring development, many projects keep variables in a .env file:
WORKSHOP_NAME=Canal Workshop
STOCK_API_KEY=sk_test_abc123Python doesn't read .env files by itself. The python-dotenv package loads one with from dotenv import load_dotenv; load_dotenv(). The file must be listed in .gitignore so it's never committed; share a .env.example with placeholder values instead. In production, use the platform's secret settings rather than a file. If a key is ever committed, treat it as leaked: revoke it and create a new one, because deleting the line doesn't remove it from history.
Testing configuration
Code that reads os.environ directly is awkward to test, because each test would need to change the real environment. Two techniques keep tests simple:
- Pass the environment in as a mapping, as
Settings.from_env(env)does below, so tests can hand over a plain dictionary. - In pytest, use the
monkeypatchfixture to set or delete variables for a single test; it restores the original values afterwards.
def test_missing_key_stops_startup(monkeypatch):
monkeypatch.delenv("STOCK_API_KEY", raising=False)
monkeypatch.setenv("STOCK_BATCH", "10")
...Test the unhappy paths too: a missing required setting, a non-numeric batch size and an out-of-range value should each produce the clear start-up error you designed.
Independent practice and a worked check
Task: define required and optional workshop settings and validate them before processing stock.
import os
def load_settings(env):
if "STOCK_API_KEY" not in env:
raise SystemExit("Set STOCK_API_KEY before running the stock import")
batch = int(env.get("STOCK_BATCH", "25"))
if batch < 1:
raise SystemExit("STOCK_BATCH must be at least 1")
return {"name": env.get("WORKSHOP_NAME", "Canal Workshop"), "batch": batch}
print(load_settings({"STOCK_API_KEY": "x", "STOCK_BATCH": "40"}))
# {'name': 'Canal Workshop', 'batch': 40}Passing a dictionary instead of os.environ makes the function easy to test with different inputs. For the worked check, os.getenv() returns a str when it finds a value; any other type must come from your own conversion.
Reference and next practice
The documentation describes os.environ and os.getenv; The Twelve-Factor App explains why configuration belongs in the environment. Continue with pip and virtual environments and API authentication. Practise with the Python exercises and Python quiz.
Environment Variables and Configuration Summary
- Environment values: Environment variables are external string settings read through os.environ or os.getenv.
- Parsing settings: Convert settings once at startup and check accepted ranges.
- Required configuration: Missing required configuration should produce an actionable startup error.
- Secret boundaries: Credentials belong outside published source and should not appear in ordinary log messages.
Environment Variables and Configuration Examples
Each example shows the code, the output it produces and what each part does.
Environment Variables and Configuration in a small program
When neither variable is set, both defaults are used.
import os
name = os.getenv("WORKSHOP_NAME", "Canal Workshop")
raw_limit = os.getenv("STOCK_BATCH", "25")
limit = int(raw_limit)
if limit < 1:
raise ValueError("STOCK_BATCH must be positive")
print(name, limit)Canal Workshop 25Line-by-line explanation
- Line 2An optional setting with a sensible default.
- Lines 3 to 4The batch size arrives as text and is converted once.
- Lines 5 to 6The converted value is checked against the application rule before use.
A validated settings object
import os
from dataclasses import dataclass
@dataclass(frozen=True)
class Settings:
workshop: str
batch: int
debug: bool
api_key: str
@classmethod
def from_env(cls, env=os.environ):
problems = []
raw_batch = env.get("STOCK_BATCH", "25")
if not raw_batch.isdigit() or int(raw_batch) < 1:
problems.append(f"STOCK_BATCH must be a positive whole number, got {raw_batch!r}")
if not env.get("STOCK_API_KEY"):
problems.append("STOCK_API_KEY is required")
if problems:
raise SystemExit("Configuration error:\n " + "\n ".join(problems))
return cls(
workshop=env.get("WORKSHOP_NAME", "Canal Workshop"),
batch=int(raw_batch),
debug=env.get("DEBUG", "").lower() in ("1", "true", "yes"),
api_key=env["STOCK_API_KEY"],
)
def __repr__(self):
return f"Settings(workshop={self.workshop!r}, batch={self.batch}, debug={self.debug}, api_key='***')"
print(Settings.from_env({"STOCK_API_KEY": "sk_live_9f2c", "DEBUG": "false"}))
try:
Settings.from_env({"STOCK_BATCH": "lots"})
except SystemExit as error:
print(error)Settings(workshop='Canal Workshop', batch=25, debug=False, api_key='***')
Configuration error:
STOCK_BATCH must be a positive whole number, got 'lots'
STOCK_API_KEY is requiredLine-by-line explanation
- Line 11Accepting a mapping makes the settings easy to test without touching the real environment.
- Lines 13 to 19All problems are collected before stopping, so they can be fixed in one go.
- Lines 28 to 29A custom
__repr__masks the key, so logging the settings never leaks it.
Common Environment Variables and Configuration Mistakes
Converting "false" with bool()
debug = bool(os.getenv("DEBUG", "false")) # True: "false" is a non-empty stringdebug = os.getenv("DEBUG", "false").strip().lower() in ("1", "true", "yes", "on")Hard-coding a secret in the source
API_KEY = "sk_live_9f2c81d0" # visible to everyone with the code, forever in Git historyimport os
API_KEY = os.environ["STOCK_API_KEY"]Environment Variables and Configuration in Real Projects
Configuration in deployed applications
The "twelve-factor app" approach, followed by most cloud platforms, stores configuration in the environment: Heroku config vars, Docker -e flags and env_file, Kubernetes ConfigMaps and Secrets, and GitHub Actions secrets all end up as environment variables your Python code reads with os.environ. Frameworks build on the same idea, such as Django settings reading os.environ, and libraries like pydantic-settings validate typed settings classes automatically.
Secret scanning tools, including GitHub's, look for API keys in pushed code and alert or block the push. Combined with .gitignore for .env files and masked logging, that keeps credentials where they belong.
Environment Variables and Configuration FAQ
What is the difference between os.environ and os.getenv?
os.environ["NAME"] raises KeyError if the variable is missing, which suits required settings. os.getenv("NAME", default) returns the default (or None) instead, which suits optional settings. Both return strings.
How do I set an environment variable for a Python script?
In macOS or Linux shells: STOCK_BATCH=50 python app.py or export STOCK_BATCH=50. In PowerShell: $env:STOCK_BATCH = "50". In Command Prompt: set STOCK_BATCH=50. Editors and IDEs also let you set variables in a run configuration.
Does Python load .env files automatically?
No. Install python-dotenv and call load_dotenv() at start-up, or let a framework or tool that supports .env files load it. Never commit the .env file itself.
How do I read a boolean environment variable?
Compare the text against accepted words: os.getenv("DEBUG", "").lower() in ("1", "true", "yes", "on"). Avoid bool(os.getenv("DEBUG")), which is True for any non-empty value, including "false".
Where should I store API keys in a Python project?
In environment variables set by your shell, IDE, CI system or hosting platform, or in a dedicated secrets manager. For local development, a git-ignored .env file is common. Never put keys in source code or print them in logs.
Environment Variables and Configuration Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
What type does os.getenv return when it finds a value?
Which review concern is valid for environment variables and configuration?
Environment Variables and Configuration Quiz
2 questions to check what you learned.
What type does os.getenv return when it finds a value?
- int
- str
- Python ignores the distinction
Which statement correctly describes required configuration?
- Missing required configuration should produce an actionable startup error.
- Only the example output matters, not the rule
- Every operation has the same behavior