Python Environment Variables and Configuration

Read settings from environment variables, convert and validate them at startup, and keep secrets out of code and logs.

Lesson 56 of 107Intermediate13 min read

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:

Python
import os
name = os.getenv("WORKSHOP_NAME", "Canal Workshop")
api_key = os.environ["STOCK_API_KEY"]     # KeyError if not set

Keeping 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:

Python
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 False

Use 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:

Python
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.

Python
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:

Python
token = os.environ["STOCK_API_KEY"]
print(f"Using API key ending ...{token[-4:]}")   # never print the full value

During development, many projects keep variables in a .env file:

Text
WORKSHOP_NAME=Canal Workshop
STOCK_API_KEY=sk_test_abc123

Python 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 monkeypatch fixture to set or delete variables for a single test; it restores the original values afterwards.
Python
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.

Python
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.

Example 1

Environment Variables and Configuration in a small program

When neither variable is set, both defaults are used.

Python
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)
Output
Canal Workshop 25
Line-by-line explanation
  1. Line 2An optional setting with a sensible default.
  2. Lines 3 to 4The batch size arrives as text and is converted once.
  3. Lines 5 to 6The converted value is checked against the application rule before use.
Example 2

A validated settings object

Python
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)
Output
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 required
Line-by-line explanation
  1. Line 11Accepting a mapping makes the settings easy to test without touching the real environment.
  2. Lines 13 to 19All problems are collected before stopping, so they can be fixed in one go.
  3. 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()

Don't
debug = bool(os.getenv("DEBUG", "false"))    # True: "false" is a non-empty string
Do
debug = os.getenv("DEBUG", "false").strip().lower() in ("1", "true", "yes", "on")

Hard-coding a secret in the source

Don't
API_KEY = "sk_live_9f2c81d0"     # visible to everyone with the code, forever in Git history
Do
import 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.

Multiple choiceeasy

What type does os.getenv return when it finds a value?

Multiple choicemedium

Which review concern is valid for environment variables and configuration?

Environment Variables and Configuration Quiz

2 questions to check what you learned.

  1. What type does os.getenv return when it finds a value?

    • int
    • str
    • Python ignores the distinction
  2. 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