Why Use Python subprocess Instead of os.system

Python's subprocess module is the modern standard for spawning external processes, replacing legacy tools like os.system(). While os.system() simply passes a command string directly to the subshell and prints output straight to the terminal, subprocess gives developers complete control over execution. Transitioning to subprocess provides essential improvements in security, stream redirection, robust error handling, and process management.

Enhanced Security Against Command Injection

The primary drawback of os.system() is its susceptibility to shell injection vulnerabilities. It requires commands to be passed as a single string and always runs through the system shell. If user input is concatenated into this string, malicious commands can be executed.

In contrast, subprocess.run() defaults to shell=False and accepts commands as a list of arguments:

import subprocess

# Safe: Arguments are properly escaped and not parsed by a shell
subprocess.run(["ls", "-l", user_directory])

Because the arguments are passed directly to the operating system's execution API, input cannot escape into arbitrary shell commands.

Complete Control Over Input and Output Streams

With os.system(), the standard output (stdout) and standard error (stderr) are sent directly to the system console, making it difficult to capture and parse the command's results within Python.

The subprocess module allows you to capture, suppress, or redirect standard streams effortlessly:

result = subprocess.run(["git", "status"], capture_output=True, text=True)
print(result.stdout)  # Access the command output as a string

You can also pipe the output of one process directly into another using subprocess.PIPE, enabling complex shell-like pipelines natively in Python.

Reliable Exit Codes and Exception Handling

os.system() returns a 16-bit value that encodes both the process exit code and the signal that killed it, requiring bit-shifting to interpret correctly across different platforms.

subprocess simplifies this by exposing the exact return code through result.returncode. Furthermore, it supports automatic error raising using the check=True argument:

try:
    subprocess.run(["false"], check=True)
except subprocess.CalledProcessError as e:
    print(f"Command failed with exit code {e.returncode}")

Timeout Management

A common issue with os.system() is that a hanging external command will block the calling Python script indefinitely. subprocess includes a built-in timeout parameter that automatically terminates the child process and raises a subprocess.TimeoutExpired exception if execution exceeds a specified duration:

subprocess.run(["sleep", "30"], timeout=5)

Asynchronous Execution and Process Control

While subprocess.run() covers most synchronous needs, the underlying subprocess.Popen class allows for advanced, asynchronous process interaction. You can launch background tasks, poll them periodically, send signals, or terminate them on demand—capabilities entirely absent from os.system().