Report this

What is the reason for this report?

Python os.system() vs subprocess: Run System Commands

Updated on March 18, 2026
Anish Singh Walia

By Anish Singh Walia

Sr Technical Writer and Team Lead

Python os.system() vs subprocess: Run System Commands

Introduction

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.

Key takeaways

  • subprocess.run() is the recommended way to execute system commands in Python 3.5 and later, replacing both os.system() and subprocess.call().
  • Avoid shell=True unless you specifically need shell features like pipes or wildcards. Passing a list of arguments without the shell prevents command injection vulnerabilities.
  • Use capture_output=True with subprocess.run() to capture stdout and stderr as strings or bytes. This gives you full control over command output.
  • Set check=True to automatically raise a CalledProcessError when a command returns a non-zero exit code, making error handling straightforward.
  • For advanced scenarios like long-running processes or real-time output streaming, use subprocess.Popen with .communicate(), .poll(), or .wait().

What are Python system commands?

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.

Using os.system() to run shell commands

The 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():

  • No output capture. The command’s output goes directly to the console. You cannot store it in a variable.
  • Limited error handling. You only get the exit code. There is no way to capture stderr separately.
  • Shell execution. The command string is passed directly to the shell (/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.

Using subprocess.call() to execute commands

The 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() 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.

Basic usage

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.

Using check=True for automatic error raising

When 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.

Setting a timeout

You 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.

Understanding shell=True and its security risks

When 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.

The shell=True security problem

Using 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 alternatives

The 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.

Using subprocess.Popen for advanced process control

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.

Basic 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

Polling a long-running process

Use .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

Piping between processes

Popen 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.

When to use 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:

  • Stream output from a process in real time
  • Send input to a running process interactively
  • Run multiple processes in parallel
  • Build pipelines between processes

Redirecting stdout, stderr, and stdin

The subprocess module provides fine-grained control over standard streams through the stdout, stderr, and stdin parameters.

Capturing output separately

import 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.

Redirecting output to a file

import 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.

Combining stdout and stderr

To 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.

Sending input to a command

Use 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.

Handling return codes and errors

Every 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.

Checking the return code manually

import 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)

Automatic error raising with check=True

As 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)']

Common 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.

Comparison table: 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.

FAQs

1. What is the difference between os.system() and subprocess.run() in Python?

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().

2. Is os.system() deprecated in Python 3?

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.

3. When should I use shell=True in subprocess?

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.

4. How do I capture the output of a shell command in Python?

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.

5. What does a non-zero return code mean in subprocess?

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.

Conclusion

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.

Next steps

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:

Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.

Learn more about our products

About the author

Anish Singh Walia
Anish Singh Walia
Author
Sr Technical Writer and Team Lead
See author profile

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

Category:
Tags:

Still looking for an answer?

Was this helpful?

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

with python 2 no need to decode ,

- abdallah

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

Creative CommonsThis work is licensed under a Creative Commons Attribution-NonCommercial- ShareAlike 4.0 International License.
Join the Tech Talk
Success! Thank you! Please check your email for further details.

Please complete your information!

The developer cloud

Scale up as you grow — whether you're running one virtual machine or ten thousand.

Get started for free

Sign up and get $200 in credit for your first 60 days with DigitalOcean.*

*This promotional offer applies to new accounts only.