Sr Technical Writer and Team Lead

Python provides several ways to run system commands from within your code. Whether you need to list files, call external tools, or automate shell workflows, the standard library includes modules that handle all of these scenarios. The two primary approaches are the older os.system() function and the modern subprocess module, which offers subprocess.run(), subprocess.call(), subprocess.Popen(), and related utilities.
This tutorial walks through each method with practical examples, explains when to use each one, and covers important topics like output capture, error handling, return codes, and security risks associated with shell=True. By the end, you will know which approach fits your use case and how to run external commands safely from Python.
subprocess.run() is the recommended way to execute system commands in Python 3.5 and later, replacing both os.system() and subprocess.call().shell=True unless you specifically need shell features like pipes or wildcards. Passing a list of arguments without the shell prevents command injection vulnerabilities.capture_output=True with subprocess.run() to capture stdout and stderr as strings or bytes. This gives you full control over command output.check=True to automatically raise a CalledProcessError when a command returns a non-zero exit code, making error handling straightforward.subprocess.Popen with .communicate(), .poll(), or .wait().A Python system command is any call from Python code that asks the operating system to execute an external program or shell instruction. This includes running utilities like ls, git, curl, or any other command-line tool installed on the machine.
Python supports this through several built-in functions and modules:
os.system() passes a command string to the system shell. It is the simplest approach but provides no access to the command’s output.subprocess.call() runs a command and returns its exit code. It is part of the subprocess module and is considered a step up from os.system().subprocess.run() is the modern, recommended function (introduced in Python 3.5). It returns a CompletedProcess object with the return code, captured output, and error streams.subprocess.Popen() provides the most control. It lets you interact with a running process in real time, stream output line by line, and manage stdin/stdout/stderr pipes directly.The rest of this tutorial covers each of these methods in detail with working code examples.
os.system() to run shell commandsThe os.system() function executes a command string through the system shell and returns the exit status of the command. It is the most basic way to run an external command from Python.
Here is a simple example that checks the installed Python version:
import os
cmd = "python3 --version"
returned_value = os.system(cmd)
print("returned value:", returned_value)
Output:
Python 3.14.3
returned value: 0
A return value of 0 means the command ran successfully. Any non-zero value signals an error.
There are a few things to keep in mind with os.system():
stderr separately./bin/sh on Unix, cmd.exe on Windows), which introduces potential security risks if any part of the command comes from user input.The Python os module documentation itself recommends using the subprocess module instead. The os.system() function remains available for backward compatibility, but subprocess.run() is the better choice for new code.
subprocess.call() to execute commandsThe subprocess.call() function is part of the subprocess module and works similarly to os.system(). It runs a command and returns its exit code. The difference is that it gives you more control over how the command is invoked.
import subprocess
cmd = "python3 --version"
returned_value = subprocess.call(cmd, shell=True)
print("returned value:", returned_value)
Output:
Python 3.14.3
returned value: 0
You can also pass the command as a list of arguments, which avoids using the shell entirely:
import subprocess
returned_value = subprocess.call(["python3", "--version"])
print("returned value:", returned_value)
Output:
Python 3.14.3
returned value: 0
Passing a list is safer because it bypasses shell interpretation. The command and its arguments are sent directly to the operating system without any shell parsing, which eliminates the risk of command injection.
While subprocess.call() improves on os.system(), it still does not let you capture output. For that, you need subprocess.run() or subprocess.check_output().
subprocess.run(): the recommended modern approachsubprocess.run() was introduced in Python 3.5 and is the recommended way to run external commands. It returns a CompletedProcess object containing the return code, captured standard output, and captured standard error.
import subprocess
result = subprocess.run(["echo", "Hello from subprocess"], capture_output=True, text=True)
print("stdout:", result.stdout.strip())
print("returncode:", result.returncode)
Output:
stdout: Hello from subprocess
returncode: 0
The capture_output=True parameter tells Python to collect both stdout and stderr. Setting text=True returns output as strings instead of raw bytes.
check=True for automatic error raisingWhen you pass check=True, Python raises a subprocess.CalledProcessError if the command exits with a non-zero return code:
import subprocess
try:
result = subprocess.run(
["ls", "/nonexistent_path"],
capture_output=True,
text=True,
check=True
)
except subprocess.CalledProcessError as e:
print(f"Command failed with return code: {e.returncode}")
print(f"stderr: {e.stderr.strip()}")
Output:
Command failed with return code: 1
stderr: ls: /nonexistent_path: No such file or directory
This pattern is much cleaner than manually checking return codes after every command.
timeoutYou can set a maximum execution time for a command using the timeout parameter (in seconds):
import subprocess
try:
result = subprocess.run(["sleep", "10"], capture_output=True, text=True, timeout=2)
except subprocess.TimeoutExpired as e:
print(f"Command timed out after {e.timeout} seconds")
Output:
Command timed out after 2 seconds
Timeouts are useful when calling external services or commands that might hang.
shell=True and its security risksWhen you pass shell=True to any subprocess function, the command is executed through the system shell (/bin/sh on Unix, cmd.exe on Windows). This lets you use shell features like pipes, wildcards, and environment variable expansion:
import subprocess
result = subprocess.run("echo $HOME", shell=True, capture_output=True, text=True)
print(result.stdout.strip())
This prints your home directory because the shell interprets $HOME.
shell=True security problemUsing shell=True with untrusted input creates a command injection vulnerability. Consider this dangerous pattern:
import subprocess
user_input = "hello; rm -rf /tmp/testdir"
subprocess.run(f"echo {user_input}", shell=True)
The semicolon in the user input causes the shell to execute a second, unintended command. This is a well-known attack vector in systems that accept user-supplied data.
shell=True safe alternativesThe safest approach is to pass commands as a list of arguments without shell=True:
import subprocess
user_input = "hello; rm -rf /tmp/testdir"
result = subprocess.run(["echo", user_input], capture_output=True, text=True)
print(result.stdout.strip())
Output:
hello; rm -rf /tmp/testdir
Here, the entire string is treated as a single argument to echo. The shell never interprets the semicolon.
If you need to split a command string into a list of arguments safely, use shlex.split():
import shlex
cmd = 'grep -r "search term" /var/log'
args = shlex.split(cmd)
print(args)
Output:
['grep', '-r', 'search term', '/var/log']
The shlex.split() function handles quoted strings and escape characters correctly, producing a list you can pass directly to subprocess.run().
When is shell=True acceptable? Only use it when you explicitly need shell features (like pipes or globbing) and you fully control the command string with no user-supplied input.
subprocess.Popen gives you direct control over a child process. Unlike subprocess.run(), which waits for the command to finish before returning, Popen starts the process and lets you interact with it while it runs.
Popen usage with communicate()The .communicate() method sends input to the process and waits for it to complete, returning a tuple of (stdout, stderr):
import subprocess
process = subprocess.Popen(
["echo", "hello from Popen"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
stdout, stderr = process.communicate()
print("stdout:", stdout.strip())
print("returncode:", process.returncode)
Output:
stdout: hello from Popen
returncode: 0
processUse .poll() to check if a process is still running without blocking:
import subprocess
import time
process = subprocess.Popen(["sleep", "3"])
while process.poll() is None:
print("Process is still running...")
time.sleep(1)
print(f"Process finished with return code: {process.returncode}")
Output:
Process is still running...
Process is still running...
Process is still running...
Process finished with return code: 0
processesPopen lets you chain commands together, replicating shell pipes:
import subprocess
p1 = subprocess.Popen(["echo", "apple\nbanana\ncherry"], stdout=subprocess.PIPE)
p2 = subprocess.Popen(["grep", "banana"], stdin=p1.stdout, stdout=subprocess.PIPE, text=True)
p1.stdout.close()
output, _ = p2.communicate()
print("Filtered output:", output.strip())
Output:
Filtered output: banana
This approach is safer than using shell=True with a pipe character because each command runs in its own process with no shell interpretation.
Popen vs run()Use subprocess.run() for most cases where you run a command and wait for the result. Choose Popen when you need to:
stdout, stderr, and stdinThe subprocess module provides fine-grained control over standard streams through the stdout, stderr, and stdin parameters.
output separatelyimport subprocess
result = subprocess.run(
["ls", "/tmp", "/nonexistent"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
print("stdout:", result.stdout[:80])
print("stderr:", result.stderr.strip())
This captures standard output and standard error into separate variables, letting you handle success output and error messages independently.
output to a fileimport subprocess
with open("output.txt", "w") as f:
subprocess.run(["echo", "writing to file"], stdout=f)
The command output goes directly to output.txt instead of the console.
stdout and stderrTo merge standard error into standard output, use stderr=subprocess.STDOUT:
import subprocess
result = subprocess.run(
["ls", "/tmp", "/nonexistent"],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True
)
print("combined output:", result.stdout)
This is useful when you want a single stream of all output from a command.
input to a commandUse the input parameter to pass data to a command’s standard input:
import subprocess
result = subprocess.run(
["grep", "world"],
input="hello\nworld\nfoo\n",
capture_output=True,
text=True
)
print("matched:", result.stdout.strip())
Output:
matched: world
For more complex input handling with the os module, see the Python os module tutorial.
return codes and errorsEvery external command returns an exit code. A return code of 0 means success; any non-zero value indicates an error. Python gives you several ways to check and respond to these codes.
return code manuallyimport subprocess
result = subprocess.run(["ls", "/nonexistent"], capture_output=True, text=True)
if result.returncode != 0:
print(f"Command failed (exit code {result.returncode})")
print(f"Error: {result.stderr.strip()}")
else:
print("Success:", result.stdout)
check=TrueAs shown earlier, check=True raises subprocess.CalledProcessError for non-zero exit codes. Here is a full example with proper exception handling:
import subprocess
try:
result = subprocess.run(
["python3", "-c", "import sys; sys.exit(42)"],
capture_output=True,
text=True,
check=True
)
except subprocess.CalledProcessError as e:
print(f"Return code: {e.returncode}")
print(f"Command: {e.cmd}")
except FileNotFoundError:
print("The command was not found on this system")
except subprocess.TimeoutExpired:
print("The command took too long to complete")
Output:
Return code: 42
Command: ['python3', '-c', 'import sys; sys.exit(42)']
exception types and how to fix them| Exception | When it occurs | How to fix |
|---|---|---|
subprocess.CalledProcessError |
Command returns a non-zero exit code (with check=True) |
Investigate the command output via e.stderr/e.output. Double-check command arguments, file paths, and permissions. The fix often involves correcting the command or input data. |
FileNotFoundError |
The executable does not exist on the system | Make sure the command or executable is installed and available in the system’s PATH. Use which <command> or install missing binaries as needed. Verify spelling of the program name. |
subprocess.TimeoutExpired |
Command exceeds the specified timeout value |
Increase the timeout value if your command regularly takes longer to run, or optimize the command to complete more quickly. Optionally, handle cleanup/retry if timeouts are expected. |
PermissionError |
Executable exists but cannot be run due to insufficient permissions | Check file permissions with ls -l (on Unix) or via the file’s Properties (on Windows). Use chmod +x <file> to make a file executable, or run the script with elevated privileges (e.g., sudo, or as an Administrator on Windows) if necessary. |
Anticipating and handling these exceptions helps make your automation robust, especially when your scripts may encounter different environments or missing dependencies. For more on effective Python error handling, see the how to handle errors in Python tutorial.
os.system vs subprocess methods| Feature | os.system() |
subprocess.call() |
subprocess.run() |
subprocess.Popen() |
|---|---|---|---|---|
| Capture stdout | No | No | Yes (capture_output=True) |
Yes (stdout=PIPE) |
| Capture stderr | No | No | Yes (capture_output=True) |
Yes (stderr=PIPE) |
| Return code access | Yes (return value) | Yes (return value) | Yes (result.returncode) |
Yes (process.returncode) |
| Raise on error | No | No | Yes (check=True) |
No (manual check) |
| Timeout support | No | Yes (timeout param) |
Yes (timeout param) |
Manual (.wait(timeout)) |
| Send input (stdin) | No | No | Yes (input param) |
Yes (stdin=PIPE) |
| Real-time streaming | No | No | No | Yes |
| Shell required | Yes (always) | Optional | Optional | Optional |
| Recommended for new code | No | No | Yes | For advanced cases |
For most tasks, subprocess.run() provides the right balance of simplicity and control. Reach for Popen only when you need real-time interaction with a running process.
os.system() passes a command string to the system shell and returns only the exit code. It cannot capture the command’s output or error messages. subprocess.run(), introduced in Python 3.5, returns a CompletedProcess object with the return code, stdout, and stderr. It also supports timeouts, automatic error raising with check=True, and can avoid the shell entirely when you pass a list of arguments. The Python documentation recommends subprocess.run() as the replacement for os.system().
No, os.system() is not formally deprecated. It still works in Python 3.14 and returns the exit status of the command. However, the official Python documentation notes that the subprocess module provides “more powerful facilities for spawning new processes” and recommends using subprocess.run() instead.
Use shell=True only when you need shell-specific features like pipes (|), wildcards (*), or environment variable expansion ($HOME), and you fully control the command string. Never use shell=True when any part of the command comes from user input, because this creates a command injection vulnerability. The safer pattern is to pass a list of arguments (e.g., subprocess.run(["ls", "-la"])) that bypasses the shell entirely.
Use subprocess.run() with capture_output=True and text=True:
import subprocess
result = subprocess.run(["date"], capture_output=True, text=True)
print(result.stdout.strip())
This stores the command’s standard output in result.stdout as a string. Without text=True, the output is returned as a bytes object. You can also use the older subprocess.check_output() function, which returns the output directly but raises an exception on non-zero exit codes.
A non-zero return code means the command did not complete successfully. The specific value depends on the command. For example, grep returns 1 when it finds no matches, and ls returns 2 for serious errors like invalid options. You can check the return code via result.returncode after calling subprocess.run(). If you pass check=True, Python will raise a subprocess.CalledProcessError automatically when the return code is non-zero, which simplifies error handling.
Python’s subprocess module gives you full control over running system commands, from simple one-liners to complex pipelines with real-time output streaming. For new projects, use subprocess.run() as your default. It handles output capture, error checking, and timeouts in a single function call. Reserve subprocess.Popen() for cases that require interacting with a running process, and avoid os.system() in new code since it lacks output capture and proper error handling.
When working with external commands, always prefer passing arguments as a list over using shell=True to protect against command injection. Combine check=True with proper exception handling to write scripts that fail predictably and provide clear error messages.
For working with command-line arguments in your own Python scripts, or for debugging complex programs, the DigitalOcean community has additional tutorials that build on these concepts.
If you are building Python automation scripts that run shell commands on remote servers, DigitalOcean Droplets provide a fast way to spin up Linux environments for testing and production. You can deploy a Droplet with Python pre-installed and use the techniques from this tutorial to automate server administration tasks.
Here are some related resources to continue learning:
subprocess and os.Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
I help Businesses scale with AI x SEO x (authentic) Content that revives traffic and keeps leads flowing | 3,000,000+ Average monthly readers on Medium | Sr Technical Writer(Team Lead) @ DigitalOcean | Ex-Cloud Consultant @ AMEX | Ex-Site Reliability Engineer(DevOps)@Nutanix
I am trying to use the check_output command to call “git branch -r --merged” as follows: import subprocess import os import sys import csv cmd = “git branch -r --merged” listed_merged_branches = subprocess.check_output(cmd) print (listed_merged_branches.decode(“utf-8”)) i am getting callprocesserror Could you help please ?
- Priyasha
I’m pretty new to python scripting, I’m trying to achieve the python equivalent of shell cmd = “echo -e “abc\ncde” >file1” The contents of file1 then looks like this: abc cde My python script has: cmd = “echo -e \“abc\ncde\” >file” os.system(cmd) However, when executing this my file looks like this: -e abc cde -e is an option for echo to recognise \n as new line character and should not be written to the file. Is there a way around this?
- divya
If you just wanted to accomplish that then you could just do this: >>> text=‘abc\ncde’ >>> print(text,file=open(‘file1’,‘w’)) The file ‘file1’ will be saved to the current working directory. You can view this by: >>> import os >>> os.getcwd()
- adk
someone can share me the python os system need to try it this my mail.marckeef@hotmail.com
- M
Hello, what if after executing command, cmd prompts something like “Press any key to continue or ctrl-c to cancel” and I basically need to somehow send any key press. Thanks for post.
- Paulius
I want include a variable which stores the result of previous command in subprocess.Popen({“command here”],subprocess.PIPE) example: state = “here” cmd = [“”" grep -i $state /log/messages"“”]
- shruhthilaya
Q. import subprocess cmd = “ps -ef | wc -l” returned_output = subprocess.check_output(cmd) print(returned_output.decode(“utf-8”)) not working in Case command using with pipe
- uttam kumar
Hey Guys, I am using python3.6.9, need some help here - I am coming from a perl background, trying to find a way to run a command (say for example ‘df’ command and store the output of columns 2 and 6 (size and name) in a key,value pair in a hash(dict in python world) and/or push these into a list which can be called from anywhere in the script- I have not seen very many examples of this kind of logic - any help is appreciated. Thanks
- Ubaid
how to store os.system output to a variable and print later is it possible
- krishna
Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.
Full documentation for every DigitalOcean product.
The Wave has everything you need to know about building a business, from raising funding to marketing your product.
Stay up to date by signing up for DigitalOcean’s Infrastructure as a Newsletter.
New accounts only. By submitting your email you agree to our Privacy Policy
Scale up as you grow — whether you're running one virtual machine or ten thousand.
Sign up and get $200 in credit for your first 60 days with DigitalOcean.*
*This promotional offer applies to new accounts only.