Subprocess in practice
Run external commands from Python, capture their output, check return codes, and raise immediately on failure with check=True.
- Run a command with subprocess.run() and capture its stdout
- Access the return code and stdout from the CompletedProcess object
- Use check=True to raise automatically when a command exits non-zero
- Decode bytes output to a string with text=True
Reading about subprocess is one thing; seeing it run is another. This lesson walks
through the practical patterns you will reach for in almost every script that calls
external tools.
Capturing stdout and checking the return code
The core pattern:
import subprocess
result = subprocess.run(
["echo", "hello world"],
capture_output=True,
text=True,
)
print(result.returncode) # 0
print(result.stdout) # "hello world\n"capture_output=True captures both stdout and stderr. text=True decodes the bytes
to a string using the system default encoding (UTF-8 on most systems). The return code
0 means the command succeeded; any non-zero value is an error by Unix convention.
Raising on failure with check=True
Without check=True, a failed command silently produces a non-zero return code and you
have to remember to check it. With check=True, subprocess.run() raises
subprocess.CalledProcessError automatically if the command exits non-zero:
try:
result = subprocess.run(
["ls", "/nonexistent-path"],
capture_output=True,
text=True,
check=True,
)
except subprocess.CalledProcessError as e:
print(f"Command failed (exit {e.returncode})")
print(f"Stderr: {e.stderr.strip()}")This keeps your scripts honest. A command that silently fails and returns junk data is much harder to debug than one that raises immediately with a clear error message.
CalledProcessError has three useful attributes: .returncode, .stdout, and
.stderr. Even when the command fails, captured output is available on the exception
object — useful for surfacing the error message from the tool.
Try it
The runner below shows the full pattern: run a command, read its output, and see what
check=True does when a command fails:
Notice that the second call raises CalledProcessError with exit code 42. Without
check=True, the call would return normally and you would need to inspect
result.returncode yourself — easy to forget under time pressure.
Processing captured output
Once you have result.stdout, it is just a string:
result = subprocess.run(
["python3", "-c", "for i in range(5): print(i)"],
capture_output=True,
text=True,
check=True,
)
lines = result.stdout.strip().split("\n")
numbers = [int(line) for line in lines]
print(sum(numbers)) # 10Strip trailing whitespace, split on newlines, and process like any other string. This is the core of most subprocess-based data pipelines.
Where to go next
Next: building pipelines — connecting the stdout of one process to the stdin of another, the way shell pipes work, but from Python where you control what happens in between.
Subprocess fundamentals
Python's subprocess module lets your scripts run external commands — understanding the difference between run() and Popen, and why shell=True is usually wrong, keeps your scripts safe and predictable.
Building pipelines
Shell pipes pass the output of one command into the input of the next — learn how to replicate this pattern in Python using Popen, and when to stay in Python vs staying in the shell.