Question

Run Shell Commands and Capture Output in Python

pythonshellsubprocess

Question

How can I write a Python function that executes a shell command and returns all of its output as a string, whether the command succeeds or fails?

For example, I want to run a command such as:

run_command("mysqladmin create test -uroot -pmysqladmin12")

If the database already exists, the function should return the message normally displayed in the terminal, such as:

mysqladmin: CREATE DATABASE failed; error: 'Can't create database 'test'; database exists'

Short Answer

You will learn how terminal programs use standard output, standard error, and exit codes, then use Python's subprocess module to run commands and capture their combined text output.

Concept

Command-line programs communicate through three important channels:

  • Standard output (stdout): normal results, such as a list of files or JSON data.
  • Standard error (stderr): diagnostics, warnings, and error messages.
  • Exit code: a numeric status returned when the command finishes. 0 usually means success; a non-zero value usually means failure.

A common surprise is that an error message is often written to stderr, not stdout. Therefore, capturing only stdout can make it appear that a failed command returned nothing.

Python's built-in subprocess module starts external programs. To collect output similar to what you see in a terminal, redirect stderr into stdout with stderr=subprocess.STDOUT. The resulting text contains both normal output and error output.

This matters when writing automation scripts, developer tools, deployment utilities, test runners, and integrations with existing command-line programs.

Mental Model

Think of a command as a worker sending you two streams of notes:

  • One results tray for ordinary information (stdout).
  • One problem tray for warnings and failures (stderr).

If you read only the results tray, you may miss the worker's explanation of what went wrong. Combining the trays lets your function return all visible messages in one string.

The exit code is the worker's final status card: 0 means the job completed successfully, while another number signals that something failed or needs attention.

Take Quiz

Syntax and Examples

Use subprocess.run() in modern Python. Pass command arguments as a list whenever possible.

import subprocess


def run_command(args):
    completed = subprocess.run(
        args,
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT,
        text=True,
        check=False,
    )
    return completed.stdout


output = run_command(["python", "--version"])
print(output)

stdout=subprocess.PIPE tells Python to collect normal output instead of immediately printing it.

stderr=subprocess.STDOUT merges error output into the same collected output.

text=True decodes the bytes produced by the program into a Python string.

check=False means that a non-zero exit code does not raise an exception. This is useful when you want to return an error message as normal function output.

To also keep the exit status, return both values:

import subprocess


def run_command(args):
    completed = subprocess.run(
        args,
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT,
        text=True,
        check=False,
    )
    return completed.returncode, completed.stdout


code, output = run_command(["mysqladmin", "create", "test", "-uroot", "-pmysqladmin12"])
print(f"Exit code: {code}")
print(output)

If mysqladmin reports that the database already exists, its error text is included in output, even though the exit code is non-zero.

Step by Step Execution

Consider this command, which writes an error message because the requested file does not exist:

import subprocess

completed = subprocess.run(
    ["python", "-c", "import sys; sys.stderr.write('File was not found\\n'); sys.exit(1)"],
    stdout=subprocess.PIPE,
    stderr=subprocess.STDOUT,
    text=True,
    check=False,
)

print(completed.returncode)
print(completed.stdout)

Execution flow:

  1. subprocess.run() starts a separate Python process.
  2. That process writes File was not found to stderr.
  3. stderr=subprocess.STDOUT redirects that text into the collected output stream.
  4. The process exits with code 1.
  5. Because check=False, Python returns a CompletedProcess object instead of raising CalledProcessError.
  6. completed.returncode is 1.
  7. completed.stdout is the string "File was not found\n".

The output text tells a human what happened; the exit code lets program logic decide whether the command succeeded.

Real World Use Cases

  • Database administration: run migration or database creation commands and record any server error text.
  • Build automation: execute tools such as npm, pytest, make, or a compiler and display their logs when a build fails.
  • Deployment scripts: run Git, Docker, or cloud CLI commands and store their output in deployment logs.
  • Health checks: call a network utility or service CLI, then inspect the return code and output.
  • Data pipelines: launch a conversion program and save diagnostics when malformed input causes a failure.
  • Developer tooling: wrap a command-line formatter, linter, or test runner in a Python application.
Take Quiz

Real Codebase Usage

In production code, developers usually need more than a raw output string.

Return output and status

A command can fail while still producing useful diagnostics. Keep the return code alongside the output:

from dataclasses import dataclass
import subprocess


@dataclass
class CommandResult:
    returncode: int
    output: str


def run_command(args: list[str]) -> CommandResult:
    completed = subprocess.run(
        args,
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT,
        text=True,
        check=False,
    )
    return CommandResult(completed.returncode, completed.stdout)

Use a guard clause for failures

result = run_command(["git", "status", "--short"])

if result.returncode != 0:
    raise RuntimeError(f"git status failed:\n{result.output}")

print(result.output)

Add a timeout

External processes can hang. Set a timeout for commands that should finish quickly:

completed = subprocess.run(
    ["python", "-c", "print('done')"],
    stdout=subprocess.PIPE,
    stderr=subprocess.STDOUT,
    text=True,
    check=False,
    timeout=10,
)

Prefer argument lists

Passing a list avoids shell parsing and reduces the risk of command injection when arguments contain user-controlled values. Use shell=True only when you intentionally need shell features and the command string is trusted.

Common Mistakes

Capturing only stdout

This often misses error messages:

# Incomplete for commands that write errors to stderr.
completed = subprocess.run(
    ["some-command"],
    stdout=subprocess.PIPE,
    text=True,
)

Fix it by combining streams:

stderr=subprocess.STDOUT

Using check=True when failures are expected

# Raises subprocess.CalledProcessError for non-zero exit codes.
subprocess.run(["some-command"], check=True)

Use check=False when your function should return the command's error output. Use check=True when failure should immediately stop the current operation.

Treating output text as proof of success

A command may print text and still fail. Always inspect returncode when success matters:

if completed.returncode != 0:
    print("The command failed")

Using shell=True with untrusted input

# Dangerous if filename comes from a user.
subprocess.run(f"cat {filename}", shell=True)

An attacker could include shell operators in filename. Prefer:

subprocess.run(["cat", filename])

Splitting arguments with .split()

# Breaks quoted values such as: --message "hello world"
args = command.split()

Build an argument list directly. If you must parse a trusted command string, use shlex.split() on Unix-like command syntax, while remembering that shell-specific syntax such as pipes is not handled unless a shell is used.

Forgetting that a command may not exist

If the executable itself is missing, Python raises FileNotFoundError; there is no process output to capture. Handle that separately if your application needs a friendly message.

Comparisons

ApproachCaptures stdoutCaptures stderrRaises on non-zero exitBest use
subprocess.run(args)NoNoNoLet output go directly to the terminal
run(..., capture_output=True, text=True)YesYes, separatelyNoYou need stdout and stderr as separate strings
run(..., stdout=PIPE, stderr=STDOUT, text=True)YesYes, combinedNoReturn terminal-like output as one string
run(..., check=True)OptionalOptionalYesStop immediately when a required command fails
run(..., shell=True)ConfigurableConfigurableNo by defaultTrusted shell expressions requiring pipes or redirection

capture_output=True is equivalent to setting both stdout=subprocess.PIPE and stderr=subprocess.PIPE, but it does not combine the streams. Use stderr=subprocess.STDOUT when ordering the output together is more useful than keeping it separate.

Take Quiz

Cheat Sheet

import subprocess

completed = subprocess.run(
    ["program", "argument"],
    stdout=subprocess.PIPE,
    stderr=subprocess.STDOUT,
    text=True,
    check=False,
)

output = completed.stdout
exit_code = completed.returncode
  • stdout: ordinary command output.
  • stderr: error and diagnostic output.
  • stderr=subprocess.STDOUT: combine both streams.
  • text=True: return strings rather than bytes.
  • returncode == 0: usual success convention.
  • check=False: return normally even if the command fails.
  • check=True: raise subprocess.CalledProcessError on failure.
  • Prefer ['program', 'argument'] over a command string.
  • Catch FileNotFoundError if the program might not be installed.
  • Add timeout=seconds for commands that could hang.
  • Use shell=True only for trusted strings that truly need shell syntax.

FAQ

How do I capture both stdout and stderr in Python?

Use stdout=subprocess.PIPE and stderr=subprocess.STDOUT in subprocess.run(). Read the combined string from completed.stdout.

Why is my captured output empty when a command fails?

Many programs write failures to stderr, not stdout. Capture stderr too, either separately or by redirecting it to STDOUT.

Does a non-zero return code prevent me from reading output?

No. With check=False, subprocess.run() returns a result object containing both returncode and captured output.

Should I use os.system() to run a command?

Usually no. os.system() provides limited output handling. subprocess.run() is clearer, safer, and gives access to return codes and streams.

How do I run a command string instead of a list of arguments?

For a trusted simple string, parse it with shlex.split(command) and pass the result to subprocess.run(). Avoid shell=True unless you need shell features such as pipes or redirects.

Is shell=True unsafe?

It can be unsafe when any part of the command comes from a user or untrusted source. Prefer an argument list so values are passed literally rather than interpreted as shell code.

What happens if the executable is not installed?

Python raises FileNotFoundError. Handle that exception if you need to return a custom error message.

Can I capture output from a command that takes too long?

Yes, but use timeout= to prevent indefinite waiting. Catch subprocess.TimeoutExpired to handle the timeout.

Take Quiz

Mini Project

Description

Build a small command runner that executes a supplied program, captures normal and error output together, and prints a clear success or failure report. This mirrors the behavior needed by automation scripts and developer tools.

Goal

Create a reusable Python function that returns a command's exit code and complete text output.

Requirements

Requirement 1

Take Quiz

Keep learning

Related questions

View more