Python's subprocess module lets you run other programs directly from your Python code. It replaced older methods like os.system and os.spawn* and works consistently across Windows, Mac, and Linux. Many developers rely on subprocess for DevOps tools, system utilities, and application wrappers because it's reliable and handles input/output streams well.
This article will show you how to use subprocess effectively in your Python applications. You'll learn to run external commands, communicate with other programs, and properly handle errors.
Prerequisites
Before starting this tutorial, you should have:
- Basic Python programming knowledge
- Python 3.8 or newer installed
- Familiarity with basic command-line concepts
Getting started with subprocess
To get the most out of this tutorial, let's create a new Python project to try out the concepts we'll discuss.
Start by creating a new directory for the project and navigate to it:
The subprocess module provides several functions for creating and interacting with subprocesses, with run() and Popen() being the most commonly used.
Let's start with the simplest example using the high-level run() function, which was introduced in Python 3.5.
Create a new file called app.py in the project directory:
Run the file using the following command:
You'll see the following output:
This example shows how subprocess.run() executes a shell command and returns a result object, including the command’s exit code. This return code is useful for understanding the outcome of the command—especially with tools like grep, where:
0means a match was found,1means no match was found,2or higher indicates an error occurred.
With the basics in place, let’s look at how to actually capture the output of a command.
Capturing output from subprocesses
In many cases, you won’t just want to run a command—you’ll want to capture and use its output in your Python code. For example, maybe you're listing files, checking a process status, or parsing command-line output for further logic.
Python’s subprocess.run() makes this easy with the capture_output parameter, which tells Python to store the command’s stdout and stderr so you can access them directly:
This code runs the ls -la command to list files in detail. Setting capture_output=True grabs both standard output and error, while text=True converts them to strings instead of bytes. You can then access the output through result.stdout.
Run it:
You'll see something like:
When you run this, you'll see the output of the ls -la command captured in the stdout attribute of the CompletedProcess object. The text=True parameter ensures that the output is decoded to a string instead of being returned as bytes.
Now that you’ve seen how to run commands and capture their output, let’s explore how to handle cases where those commands fail.
Handling command errors
External commands can fail, and your Python code should handle these failures gracefully. By default, subprocess.run() doesn't raise an exception if the command returns a non-zero exit code.
To change this behavior, use the check parameter:
In this code, check=True tells Python to raise an exception if the command fails. Since /nonexistent doesn’t exist, ls triggers a CalledProcessError, which is caught and handled to show the return code and error message.
Run the file:
When the above code runs, the ls command will fail because the directory /nonexistent doesn't exist. This will raise a subprocess.CalledProcessError exception that we catch and handle:
This pattern gives you a clean way to catch and handle command failures gracefully.
Providing input to subprocesses
Some commands expect input from standard input (stdin)—for example, tools like grep, sort, or cat. With subprocess.run(), you can pass input directly from your Python code using the input parameter.
This is useful when you want to avoid writing to a temporary file or when the data you want to process is already in memory.
Here’s a simple example:
This code passes a string to grep via standard input using the input parameter. It filters the lines and returns only those that match "test".
When text=True, the input must be a string—making it easy to work with in-memory data without writing to files.
Run the file:
You'll see the following output:
Now that you’ve seen how to pass input and capture output, let’s look at how you can structure the commands themselves.
Shell commands vs. command lists
The subprocess module supports two ways of specifying commands: as a list of arguments or as a shell command string. Let's compare the two approaches.
Update the app.py` with the following code:
This code shows two ways to run a command with subprocess. The first uses a list of arguments, which is the preferred method—it’s safer and avoids shell interpretation. The second uses a single string and sets shell=True, which runs the command through the system shell.
While both produce the same result here, shell=True can introduce security risks if the command includes user input. The command list approach is safer because it treats each argument literally, preventing shell injection.
Run the file:
You'll see the following output:
Both methods work, but as mentioned earlier, the command list approach is safer and avoids potential security issues—especially when working with user input.
To see why this matters, let’s look at a common mistake.
Create a new file called security_risk.py and add the following code:
This code shows a classic shell injection vulnerability. The intention is to display the contents of a file, but because the command is passed as a string with shell=True, the semicolon is interpreted as a command separator—and both cat and echo are executed.
Run the file:
You'll see output similar to:
Even though the file doesn’t exist, the second command (echo SECURITY BREACH) still runs. That’s because the shell interprets the semicolon as a command separator. In a real-world scenario, an attacker could use this to execute harmful commands on your system.
To prevent this, use a safer approach—pass arguments as a list instead of a shell string:
Create a file called safe_approach.py and add the following:
In this code, you're using a list to pass the command and its arguments, which avoids shell interpretation. Even if the input includes special characters, they’ll be treated as plain text rather than executable commands.
Run the file:
You might see:
This approach is much safer—especially when working with user input—because it prevents shell injection by keeping the command arguments isolated and literal.
Using Popen for advanced process control
While subprocess.run() is convenient for most use cases, the subprocess.Popen class provides more control over process execution. It allows you to:
- Start a process without waiting for it to complete
- Communicate with a process while it's running
- Control input and output streams independently
- Manage process timeouts and signals
Here's a basic example of using Popen. Update the app.py file with:
You start a subprocess in this code that runs a small inline Python script. While it sleeps for two seconds, the main program continues doing its own work. Only after that do we wait for the subprocess to finish using process.wait().
Run the file:
You'll see output like:
This example shows how Popen gives you more flexibility than run(). The subprocess begins running immediately, and your main Python program continues executing in parallel.
This is useful for non-blocking tasks like launching a background service, monitoring logs, or running multiple processes simultaneously.
Once your main code finishes, process.wait() pauses execution until the subprocess completes. You can also access the return code afterward to confirm that everything ran successfully.
Environment variables and working directories
In some cases, you may want to run a subprocess with a custom environment or from a specific directory. The subprocess.run() function is supported by both the env and cwd parameters.
Update your app.py file with the following:
Here’s what’s happening:
envdefines a custom set of environment variables passed to the subprocess. In this example, we addCUSTOM_VARand print it usingecho.cwdchanges the working directory for the subprocess. Here, we list the contents of/tmp.
Run the script:
You’ll see the custom environment output (if supported by the shell) and a directory listing from /tmp.
This approach is useful when isolating subprocesses, adjusting environment settings, or running tools that depend on a specific working directory.
Final thoughts
In this guide, you learned how to use Python’s subprocess module to run external commands, capture output, handle errors, pass input, and manage processes with precision. From simple run() calls to advanced use of Popen, you now have the tools to integrate Python with any command-line utility.
Whether you're building dev tools, automating tasks, or running system commands, subprocess gives you the control and flexibility you need—just remember to handle user input safely and avoid unnecessary use of shell=True.
If you’re ready to go further:
- Try async process handling with
asyncio.create_subprocess_exec() - Look into alternatives like
shorplumbumfor higher-level APIs - Add logging or timeouts to make subprocess usage more robust
Thanks for following along—happy scripting!