OS Environment
Reading environment variables, command-line arguments, the current working directory, and basic process information — the os module's window into the process your Python program is running as.
Every running program exists inside an operating-system process with its own environment variables, working directory, arguments, and identity. Python exposes all of this mainly through the os module (plus sys for arguments), and knowing it well is essential for writing configurable, portable, well-behaved command-line programs and services.
Environment Variables and os.environ
Environment variables are key/value string pairs set outside the program (by the shell, a container, a process manager, or a parent process) and inherited by every child process. They are the standard way to pass configuration — API keys, feature flags, deployment mode — into a program without hardcoding it or requiring command-line flags.
Environment variable values are always strings, even for things that look numeric or boolean ("1", "true") — convert them explicitly in your code.
import os
# os.environ behaves like a dict (actually os.environ is a special mapping type)
api_key = os.environ["API_KEY"] # raises KeyError if not set
db_url = os.environ.get("DATABASE_URL") # None if not set
debug = os.environ.get("DEBUG", "false") # default value if missing
print(type(os.environ)) # <class 'os._Environ'>Reading, Setting, and Deleting Environment Variables
Setting os.environ affects only the current process and processes it launches afterward (e.g. via subprocess) — it never changes the shell or parent process that started Python.
import os
os.environ["MY_VAR"] = "value" # sets it for this process and any children it spawns
current = os.environ.get("MY_VAR")
del os.environ["MY_VAR"] # removes it
os.environ.pop("MAYBE_SET", None) # remove if present, no error if notBoolean and Numeric Conversion Patterns
Because every environment variable value is a string, a common source of bugs is treating the string "False" as falsy — it isn't, since any non-empty string is truthy in Python.
import os
# WRONG mental model: bool("False") is True, because it's a non-empty string!
debug_wrong = bool(os.environ.get("DEBUG", "false")) # always True if DEBUG is set at all
# Correct: compare explicitly
debug = os.environ.get("DEBUG", "false").lower() in ("1", "true", "yes")
port = int(os.environ.get("PORT", "8000")) # convert explicitly, with a sensible defaultA Safer Pattern: A Typed Config Loader
import os
def get_bool_env(name: str, default: bool = False) -> bool:
value = os.environ.get(name)
if value is None:
return default
return value.strip().lower() in ("1", "true", "yes", "on")
def get_int_env(name: str, default: int) -> int:
value = os.environ.get(name)
if value is None:
return default
try:
return int(value)
except ValueError:
raise ValueError(f"Environment variable {name}={value!r} is not a valid integer")
DEBUG = get_bool_env("DEBUG", default=False)
PORT = get_int_env("PORT", default=8000)Command-Line Arguments: sys.argv
sys.argv (not os, but essential alongside it) is a list of strings: the program name/path at index 0, followed by each argument the user passed on the command line.
For anything beyond trivial positional arguments, use the standard-library argparse module (a separate topic) rather than parsing sys.argv by hand — it handles flags, defaults, help text, and error messages for you.
# script.py
import sys
print(sys.argv) # e.g. ['script.py', '--verbose', 'input.txt']
print(len(sys.argv))
if len(sys.argv) > 1:
filename = sys.argv[1]
else:
filename = "default.txt"The Current Working Directory
Every process has a current working directory (cwd) that relative paths are resolved against. os.getcwd() reads it; os.chdir() changes it for the current process (and, from then on, its children).
Prefer pathlib.Path for constructing and manipulating paths in new code; os.getcwd()/os.chdir() remain the standard way to read/change the process-wide working directory itself, since pathlib has no direct equivalent to chdir().
import os
from pathlib import Path
print(os.getcwd()) # e.g. '/home/user/project'
print(Path.cwd()) # pathlib equivalent, returns a Path object
os.chdir("/tmp")
print(os.getcwd()) # '/tmp'Process Information
The os module exposes basic identity information about the running process and its relationship to the OS.
import os
print(os.getpid()) # this process's ID
print(os.getppid()) # the parent process's ID
print(os.name) # 'posix' or 'nt' — which OS family
import sys
print(sys.platform) # more specific: 'linux', 'darwin', 'win32', etc.Practical Usage: A 12-Factor-Style Config Object
Reading all configuration from the environment (rather than hardcoding it) lets the exact same code run unmodified in development, staging, and production by just changing environment variables — a widely used pattern for services and containerized applications.
import os
from dataclasses import dataclass
@dataclass(frozen=True)
class Config:
debug: bool
port: int
database_url: str
def load_config() -> Config:
database_url = os.environ.get("DATABASE_URL")
if not database_url:
raise RuntimeError("DATABASE_URL environment variable is required")
return Config(
debug=os.environ.get("DEBUG", "false").lower() == "true",
port=int(os.environ.get("PORT", "8000")),
database_url=database_url,
)
config = load_config()Environment Variables and subprocess
Child processes started via subprocess inherit the parent's environment by default. Passing env= to subprocess.run()/Popen() replaces it entirely, so extending rather than replacing requires spreading os.environ explicitly.
import os
import subprocess
# Extend the current environment, don't replace it
child_env = {**os.environ, "EXTRA_VAR": "value"}
subprocess.run(["python3", "-c", "import os; print(os.environ.get('EXTRA_VAR'))"], env=child_env)Edge Cases
Environment variable names and behavior around case-sensitivity differ by OS: on POSIX systems (Linux, macOS) names are case-sensitive; on Windows they are treated case-insensitively. Modifying os.environ inside a program does not persist after the process exits — it never changes the shell that launched it, only what that process (and its children) sees. os.environ values must be strings; assigning a non-string raises TypeError.
import os
try:
os.environ["PORT"] = 8000 # TypeError: not a str
except TypeError as e:
print(e)
os.environ["PORT"] = str(8000) # correctKey Takeaways
os.environ is a dict-like mapping of environment variables; all values are strings, even for booleans/numbers.
Compare string values explicitly (e.g. against "true") rather than calling bool() on them — bool() of any non-empty string is always True.
sys.argv[0] is the program name; sys.argv[1:] are the arguments actually passed; use argparse for anything nontrivial.
os.getcwd()/os.chdir() read and change the process-wide current working directory that relative paths resolve against.
os.getpid()/os.getppid() report this process's and its parent's process IDs.
Changes to os.environ affect only the current process and its future children, never the parent shell.
debug = bool(os.environ.get("DEBUG", "false")) debug = os.environ.get("DEBUG", "false").lower() in ("1", "true", "yes") bool() of any non-empty string is True, so bool("false") is True — a classic bug. Compare the string value explicitly instead.
api_key = os.environ["API_KEY"] # crashes with KeyError if unset, unclear why api_key = os.environ.get("API_KEY") if not api_key: raise RuntimeError("API_KEY environment variable is required")
A raw KeyError gives no context; checking explicitly and raising a clear error message helps whoever deploys the program diagnose missing configuration quickly.
os.environ["PORT"] = 8000 # TypeError os.environ["PORT"] = str(8000) os.environ requires string values for both keys and values; assigning a non-string raises TypeError.
subprocess.run(["cmd"], env={"EXTRA_VAR": "value"}) # loses PATH and everything else subprocess.run(["cmd"], env={**os.environ, "EXTRA_VAR": "value"}) env= replaces the entire environment, not just adds to it; forgetting to spread the existing os.environ can break the child process (e.g. it can no longer find executables via PATH).
Why is bool(os.environ.get('FLAG', 'false')) almost always a bug?
Because the value is a non-empty string ('false'), bool() of it is True regardless of the actual intended meaning — Python's truthiness of strings depends only on whether the string is empty, not its content.
What is the difference between os.environ['KEY'] and os.environ.get('KEY')?
Indexing with [] raises KeyError if the variable isn't set; .get() returns None (or a supplied default) instead, making it safer for optional configuration.
Does setting os.environ['X'] = 'y' inside a Python script change the value in the shell that launched it?
No — a child process's environment is a copy inherited from its parent at start time; modifying os.environ only affects the current process and any processes it subsequently spawns, never the parent shell.
What is sys.argv[0], and how does it differ from the rest of sys.argv?
sys.argv[0] is the name/path of the script itself as invoked; sys.argv[1:] are the actual arguments passed by the user on the command line.
How do os.getcwd() and pathlib.Path.cwd() relate to each other?
They report the same thing — the process's current working directory — but os.getcwd() returns a plain string while Path.cwd() returns a pathlib.Path object; there is no pathlib equivalent for changing the working directory (os.chdir() is still used for that).
Why does subprocess.run(..., env={'ONLY_VAR': 'value'}) often break the child command?
env= replaces the entire environment rather than adding to it, so the child loses standard variables like PATH it needs to locate executables and libraries — you must spread the existing os.environ if you only want to add or override a few variables.
Write a function get_required_env(name) that returns the value of an environment variable, raising a clear RuntimeError (naming the missing variable) if it is not set.
import os def get_required_env(name: str) -> str: value = os.environ.get(name) if value is None: raise RuntimeError(f"Required environment variable {name!r} is not set") return value os.environ.get() returns None for a missing variable instead of raising, letting the function check explicitly and raise a descriptive error rather than a bare KeyError.
What happens when this code runs?
It raises TypeError: environment variable values are always strings, so os.environ['COUNT'] is the string '5', and '5' + 1 tries to add str and int, which Python's strong typing forbids without an explicit conversion (int(os.environ['COUNT']) + 1).
Write a function that returns True if the PORT environment variable is set to a valid integer between 1 and 65535, and False otherwise (never raising).
import os def has_valid_port() -> bool: value = os.environ.get("PORT") if value is None: return False try: port = int(value) except ValueError: return False return 1 <= port <= 65535
The function guards each failure mode explicitly: a missing variable returns False immediately, a non-numeric value is caught via ValueError from int(), and an out-of-range integer is rejected by the final comparison.
import os
os.environ["COUNT"] = "5"
value = os.environ["COUNT"] + 1