I still remember the first automation script I wrote that actually saved me real time at work — it renamed and reorganized a folder full of messy export files every morning before I got to my desk. That script leaned almost entirely on Python’s os module. If you’re doing anything that touches the file system, environment variables, or process management, you’re going to end up here sooner or later. This guide covers everything I’ve learned about os over years of using it in real projects, from beginner basics to the internal details that explain why it behaves the way it does.
What Is the os Module?
os is part of Python’s standard library, and it provides a portable way of using operating system-dependent functionality. That word “portable” is doing a lot of work — the same os function call will behave correctly whether the underlying system is Windows, Linux, or macOS, because the module abstracts away the OS-specific system calls.
import os
That single import gives me access to file operations, directory operations, environment variables, path manipulation (though I usually prefer os.path or pathlib for paths specifically), and process control.
Getting Basic System Information
The first thing I usually check in a new script is where I am and what platform I’m on:
import os
print("Current working directory:", os.getcwd())
print("Operating system name:", os.name)
print("Environment PATH variable:", os.environ.get('PATH'))
Output (on Linux, will vary by machine):
Current working directory: /home/user/projects
Operating system name: posix
Environment PATH variable: /usr/local/bin:/usr/bin:/bin
os.name returns 'posix' for Linux/macOS and 'nt' for Windows. This is useful when I need to branch logic based on platform, though for more granular platform detection I usually pair it with the platform module.
Working with Directories
Creating, listing, and removing directories are probably the most common file-system tasks I automate.
import os
# Create a single directory
os.mkdir('new_folder')
# Create nested directories (like mkdir -p)
os.makedirs('parent/child/grandchild', exist_ok=True)
# List contents of a directory
print(os.listdir('.'))
# Remove an empty directory
os.rmdir('new_folder')
# Remove nested empty directories
os.removedirs('parent/child/grandchild')
I use exist_ok=True almost every time I call makedirs() now, because without it, Python raises a FileExistsError if any part of the path already exists — and in automation scripts that run repeatedly, that’s usually not the behavior I want.
Navigating the File System
import os
os.chdir('/tmp')
print("Now in:", os.getcwd())
# Walk an entire directory tree
for root, dirs, files in os.walk('/tmp/projects'):
print(f"Directory: {root}")
print(f" Subdirectories: {dirs}")
print(f" Files: {files}")
os.walk() is genuinely one of my favorite functions in the entire standard library. It performs a top-down (by default) traversal of a directory tree, yielding a 3-tuple of (dirpath, dirnames, filenames) for every directory it visits. I’ve used it to build custom backup scripts, search utilities, and duplicate file finders.
File Operations
import os
# Rename a file
os.rename('old_name.txt', 'new_name.txt')
# Remove a file
os.remove('unwanted_file.txt')
# Check if a path exists
print(os.path.exists('new_name.txt'))
# Get file size in bytes
print(os.path.getsize('new_name.txt'))
# Get file metadata
stat_info = os.stat('new_name.txt')
print(f"Size: {stat_info.st_size} bytes")
print(f"Last modified: {stat_info.st_mtime}")
os.stat() returns a os.stat_result object containing detailed metadata pulled directly from the underlying stat() system call on POSIX systems (or the equivalent on Windows). This is where the “operating system interface” part of the module really shows itself — it’s a thin, efficient wrapper over actual OS-level calls.
Path Manipulation with os.path
Even though pathlib has become the more modern, object-oriented way to handle paths, os.path is still everywhere in existing codebases, and I use it constantly for quick scripting.
import os
path = '/home/user/documents/report.pdf'
print(os.path.basename(path)) # report.pdf
print(os.path.dirname(path)) # /home/user/documents
print(os.path.splitext(path)) # ('/home/user/documents/report', '.pdf')
print(os.path.join('folder', 'subfolder', 'file.txt')) # cross-platform join
print(os.path.abspath('report.pdf'))
print(os.path.isfile(path))
print(os.path.isdir('/home/user/documents'))
os.path.join() is one I insist on using instead of manually concatenating strings with slashes, because it automatically uses the correct separator for the current OS (/ on Linux/macOS, \ on Windows).
Internal Working: How os Talks to the Operating System
Under the hood, most os module functions are thin wrappers around C library calls and system calls provided by the OS kernel. For example, os.mkdir() on POSIX systems ultimately calls the mkdir() C library function, which itself triggers a mkdir system call that the Linux kernel handles directly. This is why os functions are fast — they’re not reimplementing file system logic in Python, they’re delegating almost immediately to the operating system’s own native routines.
This also explains why some functions behave slightly differently across platforms. os.chmod(), for instance, works fully on POSIX systems with the standard Unix permission bits, but has much more limited effect on Windows because Windows doesn’t use the same permission model.
Environment Variables
I use environment variables constantly for configuration — API keys, debug flags, deployment settings.
import os
# Read an environment variable safely
api_key = os.environ.get('API_KEY', 'default_value')
# Set an environment variable for the current process
os.environ['DEBUG_MODE'] = 'true'
# Check if a variable exists
if 'HOME' in os.environ:
print("Home directory:", os.environ['HOME'])
It’s worth knowing that os.environ changes only affect the current process and any child processes spawned afterward — they don’t persist to the shell that launched the script, and they don’t modify the system’s permanent environment variables.
Running System Commands and Processes
import os
# Run a shell command (returns exit status)
exit_code = os.system('echo Hello from the shell')
# Get process ID
print("Current PID:", os.getpid())
# Get parent process ID
print("Parent PID:", os.getppid())
I’ll be honest — I rarely use os.system() in production code anymore. The subprocess module gives far more control over input/output streams, error handling, and security (avoiding shell injection risks), so I reserve os.system() for quick, throwaway scripts.
Performance and Best Practices
For directory scanning specifically, I switched from os.listdir() combined with os.stat() calls to os.scandir() a while back, and the performance difference on large directories was noticeable. os.scandir() returns DirEntry objects that cache metadata from the underlying system call, avoiding a second round-trip to the OS for common attributes like whether an entry is a file or directory.
import os
with os.scandir('.') as entries:
for entry in entries:
if entry.is_file():
print(f"{entry.name}: {entry.stat().st_size} bytes")
This is significantly faster than os.listdir() + os.path.isfile() + os.path.getsize() for each entry, because scandir() avoids redundant system calls.
Common Mistakes I’ve Made
- Using
os.remove()on a directory. It only works on files; for directories you needos.rmdir()(empty only) orshutil.rmtree()(recursive, including non-empty). - Forgetting
exist_ok=Trueand having scripts crash on the second run. - Hardcoding path separators like
'folder' + '/' + 'file.txt'instead of usingos.path.join(), which breaks on Windows. - Not wrapping file operations in try/except. File systems are unpredictable — permissions can be denied, files can be locked by other processes, disks can fill up.
import os
try:
os.remove('might_not_exist.txt')
except FileNotFoundError:
print("File was already gone.")
except PermissionError:
print("No permission to delete this file.")
Real-World Automation Use Cases
- Batch renaming hundreds of image files based on their creation date.
- Log rotation scripts that walk a directory, find files older than N days, and archive or delete them.
- Environment-aware configuration loading — reading different config files depending on
os.environ.get('ENV'). - Build and deployment scripts that create directory structures, copy artifacts, and set permissions.
FAQs
What’s the difference between os and os.path? os handles general operating system interaction (processes, environment variables, directory operations), while os.path is specifically for path string manipulation and file existence checks.
Should I use os or pathlib for new projects? For path-heavy code, I lean toward pathlib now because of its cleaner object-oriented syntax. But os remains essential for process control, environment variables, and lower-level system calls that pathlib doesn’t cover.
Is os.system() safe to use with user input? No — it’s vulnerable to shell injection if you interpolate untrusted input into the command string. Use subprocess.run() with a list of arguments instead.
Why did os.mkdir() raise FileExistsError? Because the directory already exists and os.mkdir() doesn’t overwrite by default. Use os.makedirs(path, exist_ok=True) if that’s not the behavior you want.
Does os.walk() follow symbolic links? By default, no — it doesn’t follow symlinks to directories unless you pass followlinks=True, which helps prevent infinite loops from circular links.
Working with File Permissions
Managing permissions is another area where os gives me direct access to OS-level behavior, though I’ve learned to expect platform differences here more than almost anywhere else in the module.
import os
import stat
# Change permissions (POSIX-style octal notation)
os.chmod('script.sh', 0o755)
# Check specific permission bits
file_stat = os.stat('script.sh')
is_executable = bool(file_stat.st_mode & stat.S_IXUSR)
print(f"Owner can execute: {is_executable}")
On Linux and macOS, os.chmod() maps directly onto the POSIX chmod() system call, giving fine-grained control over read/write/execute bits for owner, group, and others. On Windows, the underlying file permission model is fundamentally different (ACL-based rather than the Unix rwx bit model), so os.chmod() there only has a limited effect — mainly toggling the read-only attribute. This is a good example of why I always test permission-related code on the actual target platform rather than assuming POSIX behavior everywhere.
Symbolic Links and Path Resolution
import os
# Create a symbolic link
os.symlink('/path/to/original.txt', '/path/to/link.txt')
# Check if a path is a symlink
print(os.path.islink('/path/to/link.txt'))
# Resolve a symlink to its real, absolute target
print(os.path.realpath('/path/to/link.txt'))
I use os.path.realpath() whenever I need to be certain I’m working with the actual underlying file rather than a link to it — this matters for things like deduplication scripts, where two different symlinks might point at the same physical file and I don’t want to process it twice.
Combining os with Other Standard Library Modules
In practice, os rarely works alone. I pair it constantly with shutil for higher-level file operations it doesn’t provide directly, like copying files or recursively deleting non-empty directories:
import os
import shutil
# os.rmdir() only removes EMPTY directories - this fails on non-empty ones
try:
os.rmdir('project_folder')
except OSError:
print("Directory not empty, using shutil instead")
shutil.rmtree('project_folder') # removes a directory and everything inside it
# Copying a file (os doesn't have a direct copy function)
shutil.copy('source.txt', 'destination.txt')
I also frequently combine os with glob for pattern-based file matching, since os.listdir() alone gives me every file with no filtering:
import glob
python_files = glob.glob('*.py')
all_python_files_recursive = glob.glob('**/*.py', recursive=True)
print(python_files)
Summary
The os module is the backbone of file system and operating system interaction in Python. It’s a direct, efficient bridge to the underlying OS’s own system calls, which makes it both fast and occasionally platform-quirky. Whether I’m walking directory trees, managing environment variables, or querying file metadata, os is almost always the first module I reach for — and understanding how thin a wrapper it really is over native system calls has helped me debug countless cross-platform issues over the years.
References
- Python Official Documentation: os — Miscellaneous operating system interfaces
- Python Official Documentation: os.path — Common pathname manipulations
- Python Official Documentation: pathlib — Object-oriented filesystem paths