Code of the Day
IntermediateShell and Processes

Subprocess in practice

Run external commands from Python, capture their output, check return codes, and raise immediately on failure with check=True.

WorkflowIntermediate8 min read
Recommended first
By the end of this lesson you will be able to:
  • 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:

Python — editable, runs in your browser

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))  # 10

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

Finished reading? Mark it complete to track your progress.

On this page