os module
import os The os module is one of Python’s most fundamental standard library modules. It provides a portable interface for interacting with the operating system, allowing you to work with files and directories, manage processes, access environment variables, and gather system information. Whether you’re building command-line tools, servers, or scripts that need to work across different operating systems, os gives you the low-level system access you need while maintaining cross-platform compatibility.
The module abstracts away platform-specific differences, so the same Python code can work on Linux, macOS, and Windows. Functions like os.path.join() handle path separators correctly on each platform, and os.stat() returns consistent stat information regardless of the underlying filesystem.
Working with Directories
os.getcwd()
Returns the current working directory as a string. This is where relative file paths resolve from.
import os
cwd = os.getcwd()
print(f"Current directory: {cwd}")
Returns: str — The absolute path of the current working directory.
While getcwd reveals where you currently are in the filesystem, changing to a different location requires chdir. Together these two functions form the foundation of directory-aware scripting — you check where you are, navigate where you need to go, and then perform operations relative to the new working directory. Once you have positioned your script in the correct directory, you typically want to inspect what that directory contains next.
os.chdir(path)
Change the current working directory to the specified path.
import os
os.chdir('/tmp')
print(f"Changed to: {os.getcwd()}")
Parameters:
| Parameter | Type | Description |
|---|---|---|
path | str | The directory path to change to |
After navigating to a target directory, examining its contents is the natural next step. Directory listing with listdir returns a flat list of entry names without any path information attached. This function is the building block for directory scanning scripts, file discovery tools, and any automation that needs to process or filter the contents of a folder before deciding what to do next.
os.listdir(path=’.’)
Return a list of entries in the given directory. Each entry is a string (just the name, not the full path).
import os
for entry in os.listdir('.'):
print(entry)
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
path | str | '.' | Directory to list |
Returns: list[str] — List of file and directory names.
While listing directories tells you what already exists, creating new directories requires mkdir. This function creates a single directory at a specified path, applying the given permission mode which is subsequently modified by the system umask. Directory creation is the first write operation in the filesystem workflow, moving from read-only inspection to actually modifying the directory structure on disk.
os.mkdir(path, mode=0o777, *, dir_fd=None)
Create a directory at the specified path with the given permission mode.
import os
os.mkdir('new_directory', mode=0o755)
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
path | str | — | Directory path to create |
mode | int | 0o777 | Permission mode (modified by umask) |
dir_fd | int | None | Directory file descriptor (Unix only) |
When you need a complete directory tree rather than a single leaf, makedirs handles the full path. The critical difference from mkdir is that makedirs recursively creates every intermediate directory that does not already exist, and the exist_ok parameter makes it safe for idempotent operations — scripts that should produce the same result whether run once or many times without raising errors on repeated runs.
os.makedirs(name, mode=0o777, exist_ok=False)
Create a directory and any necessary parent directories. Unlike mkdir(), this function creates all intermediate directories in the path.
import os
os.makedirs('project/src/utils', mode=0o755, exist_ok=True)
print("Directories created")
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
name | str | — | Directory path to create (including parents) |
mode | int | 0o777 | Permission mode |
exist_ok | bool | False | If True, don’t raise error if directory exists |
After building directory structures, cleanup is an equally important operation. Directory removal with rmdir enforces a safety constraint that prevents accidental data loss — it will only delete an empty directory, refusing to proceed if files or subdirectories remain inside. This deliberate limitation forces you to explicitly empty a directory before removing it, adding a layer of protection that many filesystem APIs omit.
os.rmdir(path)
Remove an empty directory.
import os
os.rmdir('empty_directory')
For dismantling entire directory hierarchies at once, removedirs extends the removal operation recursively. Starting from the leaf directory specified in the path, it attempts to remove each parent directory moving upward, stopping when it encounters a non-empty directory. This function pairs naturally with makedirs, providing a symmetrical way to tear down what was previously built up through the recursive creation functions.
os.removedirs(path)
Remove directories recursively. This removes the specified directory and any empty parent directories up to the root.
import os
os.removedirs('project/src/utils')
Shifting from directory operations to individual file management, the remove function deletes a single file from the filesystem. Unlike rmdir which refuses to touch directories with contents, remove targets regular files specifically and will raise an error if asked to delete a directory. The absence of a safety net here means scripts should verify that a path points to a file before calling remove, especially in code that processes user-provided paths where the type of filesystem entry may be unknown.
File Operations
os.remove(path)
Remove (delete) a file. Cannot be used to remove directories.
import os
os.remove('temp.txt')
After files are created, the ability to rename or relocate them becomes essential. The rename function handles both operations uniformly, accepting source and destination paths that can differ in both filename and directory. On the same filesystem, this operation is typically atomic, meaning no intermediate state is visible to other processes reading from the destination path at the same moment — a property that matters for concurrent scripts.
os.rename(src, dst, *, src_dir_fd=None, dst_dir_fd=None)
Rename a file or directory from src to dst.
import os
os.rename('old_name.txt', 'new_name.txt')
Parameters:
| Parameter | Type | Description |
|---|---|---|
src | str | Source path |
dst | str | Destination path |
src_dir_fd | int | Source directory file descriptor (optional) |
dst_dir_fd | int | Destination directory file descriptor (optional) |
When the destination path in a rename operation involves directories that may not yet exist, renames steps in to handle the full path. Mirroring the relationship between mkdir and makedirs, this function creates any missing intermediate directories in the destination before performing the actual rename. This is particularly useful during project restructuring where target folder hierarchies need to be established as part of the move operation itself.
os.renames(old, new)
Rename a file or directory recursively, creating any parent directories that don’t exist.
import os
os.renames('old_project/src/main.py', 'new_project/lib/main.py')
For scenarios that demand atomic file replacement, the replace function provides a guarantee that rename cannot make across all platforms. On Windows, rename raises an error when the destination file already exists, while on POSIX systems it silently overwrites. Replace eliminates this inconsistency by ensuring atomic replacement everywhere, making it the correct choice for cache files, configuration swaps, and any operation where a partial write at the destination would cause corruption of existing data.
os.replace(src, dst)
Replace a file or directory atomically. Works even if the destination exists and on some platforms can atomically replace files.
import os
os.replace('config.new', 'config.txt')
Once files are positioned correctly, metadata inspection is often the next requirement. The stat function returns a result object containing file size, modification and access timestamps, ownership identifiers, and permission bits. This rich metadata enables file comparison by timestamp, access control decisions, change detection in monitoring scripts, and any logic that must make decisions based on file properties rather than file contents.
os.stat(path, *, dir_fd=None, follow_symlinks=True)
Get status information about a file or directory. Returns a stat result object containing file size, timestamps, permissions, and more.
import os
st = os.stat('example.txt')
print(f"Size: {st.st_size} bytes")
print(f"Modified: {st.st_mtime}")
print(f"Permissions: {oct(st.st_mode)}")
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
path | str | — | File or directory path |
dir_fd | int | None | Directory file descriptor |
follow_symlinks | bool | True | Follow symbolic links |
Returns: os.stat_result — Object with attributes: st_mode, st_ino, st_dev, st_nlink, st_uid, st_gid, st_size, st_atime, st_mtime, st_ctime.
While stat gives you metadata about a single file, walk provides a complete traversal of an entire directory tree. This generator function descends recursively through every subdirectory, yielding three-part tuples containing the current directory path, its subdirectories, and its files at each level. The topdown parameter gives you control over traversal order, and the results can be filtered, accumulated, or transformed as the walk progresses through the filesystem.
os.walk(top, topdown=True, onerror=None, followlinks=False, *, dir_fd=None)
Generate a 3-tuple for each directory in the tree, walking the tree top-down or bottom-up.
import os
for dirpath, dirnames, filenames in os.walk('project'):
print(f"Directory: {dirpath}")
print(f" Subdirs: {dirnames}")
print(f" Files: {filenames}")
Yields: tuple[str, list[str], list[str]] — (dirpath, dirnames, filenames)
Moving from filesystem operations to the runtime environment, the environ object provides a dictionary-like mapping of every environment variable available to the current process. Where filesystem functions deal with paths and persistent storage, environment variables represent the execution context — database connection strings, API credentials, feature flags, and system configuration passed through the shell, container runtime, or process launcher that started your Python program.
Environment Variables
os.environ
A dictionary-like object containing the process environment variables. Both keys and values are strings.
import os
# Read an environment variable
home = os.environ.get('HOME')
print(f"Home: {home}")
# Set an environment variable
os.environ['MY_APP_CONFIG'] = 'production'
# Delete an environment variable
del os.environ['TEMP_VAR']
While direct access through environ gives you raw variable lookup, the getenv function provides a safer query path with built-in default values. This matters in production code where a missing environment variable should never cause an unexpected crash. The default parameter allows you to define fallback behavior in a single call rather than wrapping every access in a try-except block around environ dictionary lookups.
os.getenv(key, default=None)
Get an environment variable value, returning a default if the variable doesn’t exist.
import os
debug = os.getenv('DEBUG', 'false')
api_key = os.getenv('API_KEY', '')
Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
key | str | — | Environment variable name |
default | any | None | Value to return if variable doesn’t exist |
Returns: str — The environment variable value or default.
Reading environment variables is only one direction. Writing to the environment with putenv — or more idiomatically by assigning directly to environ — modifies the process environment table in place. These modifications propagate to any child processes spawned after the change, which makes environment manipulation the standard approach for configuring subprocess execution contexts in launcher scripts and build systems where children inherit the parent’s environment.
os.putenv(key, value, /)
Set an environment variable. Note: prefer modifying os.environ directly as it automatically calls putenv().
import os
os.putenv('DATABASE_URL', 'postgres://localhost/mydb')
# Equivalent and preferred:
os.environ['DATABASE_URL'] = 'postgres://localhost/mydb'
Distinct from environment variables but equally important for process execution, the executable search path determines where the operating system looks for commands. The get_exec_path function returns the ordered list of directories that the system searches when you invoke a program by name rather than by full path. Debugging command-not-found errors or verifying that required tools are reachable often starts with inspecting this list.
os.get_exec_path(env=None)
Returns the list of directories searched for executables (the PATH environment variable).
import os
path_dirs = os.get_exec_path()
print("Executable search path:")
for p in path_dirs:
print(f" {p}")
Process identity and control constitute another dimension of the os module beyond filesystem and environment handling. The most fundamental identifier is the process ID, a number assigned by the operating system that is unique during the lifetime of the process. Process IDs appear in log files, temporary file names, and monitoring dashboards, making getpid a small but frequently used function in production scripts.
Process Management
os.getpid()
Return the current process ID.
import os
print(f"Process ID: {os.getpid()}")
Every process in a Unix process tree has a parent, and knowing that parent is sometimes critical. The getppid function returns the process ID of the parent that launched your script. This information is essential for daemon processes that need to detect when the parent has exited, for child processes sending acknowledgments back to their launcher, and for any situation where understanding the process hierarchy matters for coordination.
os.getppid()
Return the parent process ID.
import os
print(f"Parent process ID: {os.getppid()}")
The simplest mechanism for launching external programs is the system function, which runs a command string in a subshell and blocks until it completes. While straightforward and familiar, system passes the command through the shell interpreter, which means shell metacharacters in the command string are interpreted. This behavior makes system convenient for quick scripts but also creates a command injection vulnerability if any part of the command string originates from untrusted input.
os.system(command)
Execute a command in a subshell. Returns the exit code of the command.
import os
exit_code = os.system('echo "Hello from shell"')
print(f"Exit code: {exit_code}")
When you need finer control over an external process than system provides, popen opens a pipe to the command and returns a file-like object. From this object you can read the command output incrementally or write input to the process while it runs. This enables processing of large outputs without loading everything into memory and supports bidirectional communication with long-running commands that system cannot handle at all due to its synchronous blocking nature.
os.popen(command, mode=‘r’, buffering=-1)
Open a pipe to or from a command. Returns a file object connected to the pipe.
import os
output = os.popen('ls -la').read()
print(output)
Signal delivery through the kill function represents the lowest level of inter-process communication available directly through the os module. Despite its ominous name, kill can send any signal defined in the signal module, not just termination signals. Graceful shutdown via SIGTERM, forced termination via SIGKILL, reload configuration via SIGHUP, and user-defined signals for custom protocols are all supported through this single function call.
os.kill(pid, sig)
Send a signal to a process. Useful for inter-process communication on Unix.
import os
import signal
# Send SIGTERM to terminate a process
os.kill(pid, signal.SIGTERM)
Process management is powerful but inherently platform-specific. Path manipulation, by contrast, is among the most portable capabilities the os module offers. The join function is the canonical way to construct filesystem paths correctly on any operating system, using the appropriate separator character for the current platform and handling edge cases like absolute paths appearing mid-way through the segment list without corrupting the result.
Path Manipulation
os.path.join(*paths)
Join one or more path components intelligently. Uses the correct separator for the platform.
import os
path = os.path.join('home', 'user', 'projects', 'app')
print(path) # 'home/user/projects/app' on Unix
After assembling a path with join, verifying that it refers to something real is the natural next step. The exists function returns a simple boolean answer, making it the standard guard clause before file operations. Checking existence before reading or writing avoids the cascade of exceptions that missing paths would otherwise generate, though race conditions between the check and the actual operation must be considered in concurrent environments where filesystem state can change between calls.
os.path.exists(path)
Return True if the path points to an existing file or directory.
import os
if os.path.exists('config.txt'):
print("Config file exists")
os.path.isfile(path)
Return True if the path is an existing regular file.
os.path.isdir(path)
Return True if the path is an existing directory.
While exists tells you whether something is there at all, isfile and isdir differentiate between file and directory entries. This distinction matters in scripts that walk directory trees or process mixed file and directory inputs, where operating on a directory as if it were a file would raise confusing errors. Knowing the type of a path entry before acting on it is a defensive programming practice that saves debugging time, especially when processing paths provided by users or external configuration files.
os.path.basename(path)
Return the base name of the file or directory path (the final component).
import os
print(os.basename('/home/user/document.txt')) # 'document.txt'
Paths contain both a filename component and the directories leading to it, and basename and dirname let you extract either part independently. Where basename gives you the leaf — the final component after the last separator — dirname returns everything that comes before it. Together they decompose a path into its two logical halves without requiring string splitting or regular expressions, which would inevitably introduce platform-specific bugs when moving code between Windows and Unix.
os.path.dirname(path)
Return the directory name of the path (everything except the final component).
import os
print(os.dirname('/home/user/document.txt')) # '/home/user'
Beyond the directory and filename split, paths often carry a file extension that needs to be examined or changed. The splitext function separates the root of a filename from its final dot-suffix, making extension-based filtering and replacement straightforward. Note that this function works on the final dot only, which means multi-part extensions like archive.tar.gz are split at the last dot — a behavior worth remembering when processing compressed archive filenames in Python scripts.
os.path.splitext(path)
Split the path into a pair (root, ext) where ext is empty or starts with a dot.
import os
root, ext = os.path.splitext('document.txt')
print(f"Root: {root}, Extension: {ext}") # Root: document, Extension: .txt
The functions covered so far handle individual operations: navigation, listing, creation, removal, metadata, environment, processes, and path manipulation. The following examples show how these building blocks combine into practical scripts. Each example demonstrates a realistic use case that ties multiple os module functions together into a cohesive workflow.
Examples
Walking a directory tree and finding files
import os
def find_files(directory, extension):
"""Find all files with a given extension."""
matches = []
for dirpath, dirnames, filenames in os.walk(directory):
for filename in filenames:
if filename.endswith(extension):
matches.append(os.path.join(dirpath, filename))
return matches
# Find all Python files in the current directory
py_files = find_files('.', '.py')
for f in py_files:
print(f)
Running this function against the current directory produces output showing every Python file found during the recursive walk through the filesystem. Each result path is assembled with os.path.join to ensure platform-correct separators, and the walk generator efficiently traverses directories without loading the entire file tree into memory at once.
./main.py
./utils/helpers.py
./tests/test_main.py
The walk example demonstrated recursive file discovery, which reads existing structures without modifying them. The following example shows the complementary write operation — creating a complete project scaffold programmatically. Where the walk script scanned downward through directories, this setup script builds upward from the base path, using makedirs with exist_ok to safely create each folder in a predefined structure list. Together these two patterns cover the full lifecycle of filesystem automation: discovering what already exists and creating what does not.
Safely creating a directory structure
import os
def setup_project_structure(base_path):
"""Create a complete project directory structure."""
structure = [
'src',
'src/utils',
'src/models',
'tests',
'tests/unit',
'tests/integration',
'docs',
'config',
]
for dir_path in structure:
full_path = os.path.join(base_path, dir_path)
os.makedirs(full_path, exist_ok=True)
print(f"Created: {full_path}")
# Usage
setup_project_structure('my_project')
When run, this function prints each directory as it is created, confirming the full project scaffold one folder at a time. The exist_ok=True parameter means the script can be executed repeatedly without raising FileExistsError, which is essential for setup scripts that may need to run multiple times during development or deployment without failing on preexisting directories.
Created: my_project/src
Created: my_project/src/utils
Created: my_project/src/models
Created: my_project/tests
Created: my_project/tests/unit
Created: my_project/tests/integration
Created: my_project/docs
Created: my_project/config
The examples above demonstrate how individual functions combine into complete scripts that solve real problems. Beyond full examples, certain patterns appear so frequently in Python codebases that they deserve recognition as reusable building blocks. The following patterns represent the most common idioms for safe and portable filesystem operations using the os module.
Common Patterns
Check before creating/removing
import os
# Check if file exists before removing
if os.path.exists('file.txt'):
os.remove('file.txt')
# Check if directory exists before creating
if not os.path.exists('output'):
os.makedirs('output')
Checking file existence before operations prevents common errors, but portable scripts must also handle path construction correctly across operating systems. The next pattern shifts focus from conditional logic to path assembly, demonstrating how os.path.join combined with environment variable expansion produces paths that work on any platform without hardcoded separators — a critical practice for scripts that run in both development and production environments.
Build paths cross-platform
import os
# Always use os.path.join for portability
config_path = os.path.join(os.environ['HOME'], '.config', 'myapp', 'settings.json')
Building portable paths ensures you can locate files correctly, but understanding what those files contain often starts with their metadata. The stat result object provides file size and timestamp information in a structured form, and converting raw timestamps to human-readable datetime objects makes the information actionable for logging, reporting, and comparison scripts that need to answer questions about when a file was last modified or how large it has grown.
Get file size and modification time
import os
from datetime import datetime
def file_info(path):
st = os.stat(path)
return {
'size': st.st_size,
'modified': datetime.fromtimestamp(st.st_mtime),
'created': datetime.fromtimestamp(st.st_ctime),
}
info = file_info('example.txt')
print(f"Size: {info['size']} bytes")
print(f"Modified: {info['modified']}")
Individual file inspection is useful, but batch processing requires iteration. The final pattern shows how listdir combined with a list comprehension filters directory contents by extension. This concise approach replaces multi-line for loops with a single expression, a common idiom in scripts that need to quickly select a subset of files for further processing without the overhead of full directory traversal.
Iterate over files with filtering
import os
# Get all .txt files in current directory
txt_files = [f for f in os.listdir('.') if f.endswith('.txt')]
print(txt_files)