PythonEnvironment Variables

Environment Variables

Environment variables are key-value pairs that live outside your code, in the operating system (or the process that launches your program). They let a single codebase behave differently depending on where it runs — locally on your laptop, in a CI pipeline, in staging, or in production — without changing a single line of source code.

Why not just hardcode configuration?

If you hardcode a database URL, an API key, or a debug flag directly into your source files, you run into two problems: the value can't change between environments without editing code, and secrets end up committed to version control where anyone with repo access (or anyone who finds a leaked copy) can read them. Environment variables solve both problems by keeping configuration and secrets out of the codebase entirely.

  • Configuration that varies per environment: database hosts, feature flags, log levels, debug mode.

  • Secrets that must never be checked into git: API keys, database passwords, signing keys, tokens.

  • Values injected by the deployment platform: PORT, PATH, cloud provider metadata.

Reading environment variables with os.environ

The os module exposes the current process's environment through os.environ, a dictionary-like object mapping variable names to string values.

Python
import os

# Direct access — raises KeyError if the variable is not set
db_host = os.environ['DB_HOST']

# Safer: os.environ.get() returns None (or a default) if missing
db_host = os.environ.get('DB_HOST')
debug = os.environ.get('DEBUG', 'false')  # default value as fallback

print(f"Connecting to {db_host}, debug={debug}")

os.getenv() is essentially an alias for os.environ.get() and is the more common idiom in real code:

Python
import os

api_key = os.getenv('API_KEY')          # None if not set
port = int(os.getenv('PORT', '8000'))   # default fallback, cast to int

if api_key is None:
    raise RuntimeError('API_KEY environment variable is required')
Note
Every value coming out of `os.environ` is a plain `str`, even for things that look numeric or boolean. Always convert explicitly — `int(os.getenv('PORT', '8000'))`, or compare strings like `os.getenv('DEBUG', 'false').lower() == 'true'`.
Setting environment variables at the OS level

Environment variables are normally set outside of Python, in the shell that launches your program, or in the configuration of whatever platform runs your app (Docker, systemd, a cloud provider's dashboard, a CI job).

Bash
# macOS / Linux (bash/zsh) — set for a single command
DEBUG=true python app.py

# Export it for the whole shell session
export DATABASE_URL="postgres://user:pass@localhost/mydb"
python app.py

# Windows PowerShell
$env:DEBUG = "true"
python app.py

Variables set this way exist only in that process (and its children) — they disappear once the shell session or container ends unless you persist them somewhere (a shell profile, a systemd unit file, a platform's environment variable settings, etc.).

The .env file approach with python-dotenv

Typing export commands every time you start work gets old fast, and it's easy to forget one. The common convention is to put local development configuration in a plain text file named .env at the project root, and load it automatically when your program starts. The python-dotenv package does exactly this.

Bash
pip install python-dotenv
.env

Bash
DATABASE_URL=postgres://user:pass@localhost/mydb
API_KEY=sk_test_1234567890
DEBUG=true

Python
from dotenv import load_dotenv
import os

load_dotenv()  # reads .env in the current directory and loads it into os.environ

database_url = os.getenv('DATABASE_URL')
api_key = os.getenv('API_KEY')
debug = os.getenv('DEBUG', 'false').lower() == 'true'

print(database_url)
print(debug)
postgres://user:pass@localhost/mydb
True

load_dotenv() only fills in variables that aren't already set in the real environment, so a variable exported by your shell or CI system will always take precedence over the same key in .env. That makes .env a safe default for local development that gets overridden cleanly in other environments.

Where load_dotenv looks, and how to be explicit

By default, load_dotenv() searches the current directory and walks upward looking for a .env file. In larger projects it's clearer to pass an explicit path:

Python
from pathlib import Path
from dotenv import load_dotenv

env_path = Path(__file__).resolve().parent / '.env'
load_dotenv(dotenv_path=env_path)
Warning
Never commit a `.env` file to git. It typically contains real secrets — API keys, database credentials, tokens — and once something is pushed to a shared repository it is effectively permanent, even if you delete it later, because it remains in the git history. Add `.env` to your `.gitignore` before you ever create the file, and commit a `.env.example` (with placeholder values only) instead so teammates know which variables are needed.
.gitignore

Bash
.env
.env.local
.env.*.local
.env.example (safe to commit)

Bash
DATABASE_URL=postgres://user:password@localhost/dbname
API_KEY=your-api-key-here
DEBUG=false
A pattern for required vs optional variables

Python
import os
from dotenv import load_dotenv

load_dotenv()


def require_env(name: str) -> str:
    value = os.getenv(name)
    if value is None:
        raise RuntimeError(f"Missing required environment variable: {name}")
    return value


DATABASE_URL = require_env('DATABASE_URL')
LOG_LEVEL = os.getenv('LOG_LEVEL', 'INFO')  # optional, has a sane default

Failing fast with a clear error message when a required variable is missing is far easier to debug than a mysterious None propagating deep into your application.

Approach

Where it lives

Typical use

export VAR=value in the shell

Current shell session / process tree

Quick local overrides, CI job configuration

.env file + python-dotenv

A file at the project root (gitignored)

Local development defaults, per-developer secrets

Platform environment settings

Docker, systemd, cloud provider's dashboard

Staging and production configuration

Tip
Many web frameworks (Django, Flask) and libraries (Pydantic's `BaseSettings`) build on top of this same idea — reading configuration from the environment — so learning `os.getenv()` and `.env` files pays off well beyond plain scripts.

Environment variables are the standard, framework-agnostic way to keep configuration flexible and secrets out of your source code. Master os.getenv() with sensible defaults, use python-dotenv for a smooth local development experience, and always keep .env out of version control.