SOFTWARE / SYSTEMS / AIEngineering news. Technical depth.
Architecture / 3 MIN READ

How to keep stderr separate from captured stdout in Python

Capture subprocess stdout while leaving stderr inherited, or capture both streams separately and retrieve their output after a nonzero exit.

Use subprocess.run() with stdout=subprocess.PIPE and leave stderr unset to capture stdout without redirecting stderr. If you need both streams afterward, use capture_output=True: it captures stdout and stderr separately rather than combining them. Stream arguments, run() reference

Capture stdout while leaving stderr inherited

Replace this example command with your program and its arguments:

import subprocess

command = ["your-tool", "--export"]

result = subprocess.run(
    command,
    stdout=subprocess.PIPE,
    text=True,
    check=True,
)

output = result.stdout

Each stream has a different destination:

  • stdout goes into a pipe.
  • stderr remains inherited because its argument is left at the default, None.

The inherited stderr destination is not necessarily a terminal; it may already be redirected elsewhere. The important distinction is that the child’s stderr does not enter the stdout capture pipe. Stream arguments

After a successful call, result.stdout contains the captured output. Because stderr was not captured, result.stderr is None. CompletedProcess reference

Use this pattern when your program needs to parse stdout while leaving messages written to stderr at their existing destination. These settings route streams, not message types: anything the command writes to stdout still belongs to captured stdout.

Capture both streams for later inspection

When your application also needs access to stderr, capture both streams. Using the same command:

result = subprocess.run(
    command,
    capture_output=True,
    text=True,
    check=True,
)

output = result.stdout
diagnostics = result.stderr

For stream routing, capture_output=True is equivalent to setting both stdout=subprocess.PIPE and stderr=subprocess.PIPE. Do not supply stdout or stderr arguments alongside capture_output=True.

run() waits for the command to finish. In this example, captured stderr becomes available to your code afterward rather than being displayed as it arrives. run() reference

After a successful call, the two streams are available separately as result.stdout and result.stderr. CompletedProcess reference

Avoid merging or discarding stderr

Two other settings serve different purposes:

  • stderr=subprocess.STDOUT merges stderr into stdout’s destination. If stdout is captured, the capture contains both streams. This defeats stream separation.
  • stderr=subprocess.DEVNULL sends stderr to the null device. It keeps stderr out of stdout, but discards it rather than preserving it for display or inspection.

If you need separate results, configure separate destinations before starting the command rather than trying to classify messages after merging them. Stream arguments

Handle nonzero exit statuses

Stream routing and exit-status handling are separate choices. With check=True, a nonzero exit status raises subprocess.CalledProcessError instead of returning a result. When both streams were captured, the exception provides their contents through exc.stdout and exc.stderr, along with exc.returncode. run() reference, CalledProcessError reference

This example forwards captured stderr to the parent’s sys.stderr before re-raising the exception:

import subprocess
import sys

command = ["your-tool", "--export"]

try:
    result = subprocess.run(
        command,
        capture_output=True,
        text=True,
        check=True,
    )
except subprocess.CalledProcessError as exc:
    if exc.stderr:
        sys.stderr.write(exc.stderr)
    raise
else:
    output = result.stdout

With the stdout-only capture pattern, stderr already goes to its inherited destination. In that case, captured stdout is still available as exc.stdout, but exc.stderr is None; there is no captured stderr to forward. Stream arguments, CalledProcessError reference

Choose text or bytes separately from stream routing

Captured output is bytes by default. These examples use text=True to obtain strings using the default text encoding. If the command’s output contract specifies an encoding, set it explicitly—for example, encoding="utf-8" for a command documented to emit UTF-8. run() reference, CompletedProcess reference

Stream routing determines where output goes; text-mode and encoding options determine how captured output is represented.

SOURCES & REVIEW

Sources are linked throughout this guide. Product capabilities can change; consult the linked documentation for your deployment.

Read our editorial approach ↗