Home› Tutorials› Standard Library
Standard LibraryPython's os Module: Environment Variables, Processes, and the Filesystem
os.environis a dict-like mapping of the process's environment variables, live at the moment your script started.os.pathstill underlies file path handling everywhere, even thoughpathlibis the friendlier modern interface for most new code.os.walk()recursively descends a directory tree, yielding a triple of directory path, subdirectories, and files at every level.os.system()is a legacy shortcut;subprocessgives you real control over input, output, and error handling for external commands.
Reading and setting environment variables
os.environ behaves like a dictionary, populated from the process's environment at startup:
import os
db_host = os.environ.get("DB_HOST", "localhost")
os.environ["DEBUG"] = "1" # visible to subprocesses this script spawns
Use .get() with a default rather than os.environ["DB_HOST"] directly unless a missing variable should genuinely crash the program — a plain KeyError from a missing environment variable is a confusing failure mode for whoever deploys the script next. Setting a key on os.environ only affects the current process and anything it spawns afterward; it does not persist to the shell that launched Python, since a child process can never modify its parent's environment.
os.path and where pathlib takes over
os.path works with plain strings and predates pathlib by two decades. It's still what a lot of existing code and other libraries expect:
full = os.path.join("data", "2026", "report.csv")
print(os.path.exists(full))
print(os.path.splitext(full)) # ('data/2026/report', '.csv')
The pathlib guide covers the object-oriented alternative in depth — Path("data") / "2026" / "report.csv" instead of nested os.path.join() calls. New code generally reads more clearly with pathlib, but os.path functions like os.path.abspath() and os.path.expanduser() still get reached for regularly, and a Path object converts to and from a plain string trivially when a function insists on one.
Listing and walking directories
os.listdir() gives the immediate contents of one directory. os.walk() goes further, recursing into every subdirectory:
for dirpath, dirnames, filenames in os.walk("project"):
for name in filenames:
if name.endswith(".py"):
print(os.path.join(dirpath, name))
Each iteration hands back the current directory path, the list of subdirectory names inside it, and the list of file names inside it — three separate lists, not one combined listing, which trips people up the first time. Mutating dirnames in place (for example, removing entries to skip a folder like .git) actually prunes the walk, since os.walk reads that same list object on its next recursive step rather than a fresh copy.
Process information: getpid, getcwd, and cpu_count
A handful of functions expose facts about the running process and machine that come up in logging, diagnostics, and scaling decisions:
print(os.getpid()) # this process's ID
print(os.getcwd()) # current working directory
print(os.cpu_count()) # logical CPU count, or None if undetermined
os.cpu_count() is worth calling out because it's a common (and reasonable) input to sizing a multiprocessing worker pool — but it can return None on some platforms or container configurations, so code that does Pool(os.cpu_count()) without a fallback can crash in an environment where the value can't be determined. A safer pattern is Pool(os.cpu_count() or 1), which degrades to a single worker rather than raising a TypeError deep inside the pool constructor.
Two more functions worth knowing for building and tearing down directory structures: os.makedirs(path, exist_ok=True) creates every missing directory in a path in one call, unlike os.mkdir() which only creates the final component and fails if its parent doesn't already exist; and os.rename(src, dst) moves or renames a file or directory in place. Both raise OSError subclasses on failure — a full disk, a permissions problem, or a destination that already exists in a way that conflicts — so wrapping calls that touch the filesystem in a deliberate try/except block, as covered in the exception handling guide, is worth doing anywhere the path isn't fully under your own control.
Why subprocess replaced os.system
os.system() runs a shell command and returns only its exit status — not its output, and it goes through the shell, which opens the door to shell injection if any part of the command string comes from untrusted input:
os.system("ls -la") # exit status only, output goes straight to the terminal
The subprocess module guide covers subprocess.run(), which captures stdout and stderr as data, supports passing arguments as a list to avoid shell parsing entirely, and reports failures in a way that's actually inspectable in code rather than just a numeric status. os.system() still shows up in quick scripts and older codebases, but new code should reach for subprocess by default. The os module documentation is worth skimming once, since it covers dozens of platform-specific functions beyond what any single article can.