The os module provides portable ways to use operating system functionality. Here are the patterns that matter.

Environment Variables

import os
 
# Get with default
debug = os.environ.get("DEBUG", "false")
api_key = os.getenv("API_KEY", "")  # Same as environ.get
 
# Check existence
if "HOME" in os.environ:
    print(f"Home: {os.environ['HOME']}")
 
# Set temporarily
os.environ["MY_VAR"] = "value"
del os.environ["MY_VAR"]
 
# Expand variables in strings
path = os.path.expandvars("$HOME/documents")

Path Operations

# Join paths (use pathlib for new code, but os.path is everywhere)
full_path = os.path.join("dir", "subdir", "file.txt")
 
# Split components
dirname = os.path.dirname("/home/user/file.txt")   # /home/user
basename = os.path.basename("/home/user/file.txt") # file.txt
root, ext = os.path.splitext("file.tar.gz")        # ('file.tar', '.gz')
 
# Normalize paths
clean = os.path.normpath("dir/../other/./file")    # other/file
absolute = os.path.abspath("relative/path")        # Full path
 
# Check path types
os.path.exists("/some/path")
os.path.isfile("/some/path")
os.path.isdir("/some/path")
os.path.islink("/some/path")

Directory Operations

# Current directory
cwd = os.getcwd()
os.chdir("/new/directory")
 
# Create directories
os.mkdir("single_dir")                     # Single level
os.makedirs("nested/path/dirs", exist_ok=True)  # Create parents
 
# Remove directories
os.rmdir("empty_dir")                      # Must be empty
os.removedirs("nested/empty/dirs")         # Remove empty parents too
 
# List contents
entries = os.listdir(".")                  # List of names

File Operations

# Remove files
os.remove("file.txt")
os.unlink("file.txt")  # Same thing
 
# Rename/move
os.rename("old.txt", "new.txt")
os.replace("old.txt", "new.txt")  # Atomic, cross-filesystem
 
# File stats
stat_result = os.stat("file.txt")
print(stat_result.st_size)   # Size in bytes
print(stat_result.st_mtime)  # Modification time
 
# Permissions
os.chmod("file.txt", 0o755)
os.chown("file.txt", uid, gid)  # Unix only

Walking Directories

# Classic pattern
for root, dirs, files in os.walk("."):
    # Skip hidden directories
    dirs[:] = [d for d in dirs if not d.startswith(".")]
    
    for file in files:
        full_path = os.path.join(root, file)
        print(full_path)
 
# Bottom-up (for deletions)
for root, dirs, files in os.walk(".", topdown=False):
    for file in files:
        os.remove(os.path.join(root, file))
    for d in dirs:
        os.rmdir(os.path.join(root, d))

Process Information

# Current process
print(os.getpid())    # Process ID
print(os.getppid())   # Parent process ID (Unix)
print(os.getuid())    # User ID (Unix)
print(os.getgid())    # Group ID (Unix)
 
# CPU info
print(os.cpu_count())  # Number of CPUs
 
# System name
print(os.name)        # 'posix', 'nt', 'java'
print(os.uname())     # Detailed system info (Unix)

Executing Commands

# Simple command (avoid for new code - use subprocess)
exit_code = os.system("ls -la")
 
# Better: get output
import subprocess
result = subprocess.run(["ls", "-la"], capture_output=True, text=True)
print(result.stdout)
 
# Environment for subprocess
env = os.environ.copy()
env["MY_VAR"] = "value"
subprocess.run(["command"], env=env)

File Descriptors

# Low-level operations (rarely needed)
fd = os.open("file.txt", os.O_RDONLY)
try:
    data = os.read(fd, 1024)
finally:
    os.close(fd)
 
# Duplicate file descriptors
new_fd = os.dup(fd)
 
# Redirect stdout
saved = os.dup(1)
os.dup2(new_fd, 1)
# ... operations go to new_fd ...
os.dup2(saved, 1)  # Restore

Temporary Files

# Get temp directory
tmp = os.environ.get("TMPDIR", "/tmp")
 
# Or use tempfile module (preferred)
import tempfile
with tempfile.NamedTemporaryFile(delete=False) as f:
    f.write(b"data")
    temp_path = f.name
# Create symlink
os.symlink("target.txt", "link.txt")
 
# Read link target
target = os.readlink("link.txt")
 
# Check if symlink (doesn't follow)
os.path.islink("link.txt")  # True
os.path.exists("link.txt")  # True if target exists
 
# Stat without following
os.lstat("link.txt")

Platform Detection

import os
import sys
 
# Basic platform
if os.name == "nt":
    print("Windows")
elif os.name == "posix":
    print("Unix-like")
 
# More specific
if sys.platform == "darwin":
    print("macOS")
elif sys.platform.startswith("linux"):
    print("Linux")
elif sys.platform == "win32":
    print("Windows")

Common Recipes

def ensure_dir(path):
    """Create directory if it doesn't exist."""
    os.makedirs(path, exist_ok=True)
 
def safe_delete(path):
    """Delete file if it exists."""
    try:
        os.remove(path)
    except FileNotFoundError:
        pass
 
def find_files(directory, extension):
    """Find all files with given extension."""
    results = []
    for root, _, files in os.walk(directory):
        for f in files:
            if f.endswith(extension):
                results.append(os.path.join(root, f))
    return results
 
def get_size(path):
    """Get total size of file or directory."""
    if os.path.isfile(path):
        return os.path.getsize(path)
    total = 0
    for root, _, files in os.walk(path):
        for f in files:
            total += os.path.getsize(os.path.join(root, f))
    return total

Access Checks

# Check permissions before trying
if os.access("file.txt", os.R_OK):
    # Can read
    pass
 
if os.access("file.txt", os.W_OK):
    # Can write
    pass
 
if os.access("/usr/bin/python", os.X_OK):
    # Can execute
    pass
 
# Combined check
if os.access("file.txt", os.R_OK | os.W_OK):
    # Can read and write
    pass

For new code, prefer pathlib for path operations and subprocess for running commands. But os remains essential for environment variables, process info, and low-level operations.

React to this post: