Running Commands with subprocess

Reviewed & published by Brayan K

The subprocess module lets your Python program launch external commands, capture their output and error text, and check whether they succeeded.

Part of the free Python course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

It is how you call git, run a build tool, convert a file, or glue Python to any command-line program — safely, with full control over input, output, and failures.

What You'll Learn in This Lesson

1 subprocess.run and Capturing Output

The one function you need is subprocess.run. Pass the command as a list of strings, add capture_output=True to grab its output, and text=True so you get strings instead of raw bytes:

import subprocess

result = subprocess.run(
    ["echo", "hello world"],
    capture_output=True,
    text=True,
)

print(result.returncode)     # 0      (0 means success)
print(repr(result.stdout))   # 'hello world\n'
print(repr(result.stderr))   # ''     (nothing went to stderr)

The returned object is a CompletedProcess. Its three useful attributes are .returncode (the exit status), .stdout (normal output), and .stderr (error output). You can run Python itself this way using sys.executable, which points at the current interpreter:

import subprocess, sys

result = subprocess.run(
    [sys.executable, "-c", "print(2 + 2)"],
    capture_output=True,
    text=True,
)
print(result.stdout.strip()) # 4
import subprocess, sys

# Run the current Python interpreter to print a result
result = subprocess.run(
    [sys.executable, "-c", "print('from a child process')"],
    capture_output=True,
    text=True,
)

print("returncode:", result.returncode)   # returncode: 0
print("stdout:", result.stdout.strip())    # stdout: from a child process
print("stderr is empty:", result.stderr == "")  # stderr is empty: True

# ✅ Expected output:
# returncode: 0
# stdout: from a child process
# stderr is empty: True

2 Return Codes, stderr, and check=True

A non-zero return code means the command failed. You can inspect .returncode yourself, or let check=True raise a CalledProcessError automatically — the same "fail loudly" idea as an exception:

import subprocess, sys

# A command that writes to stderr and exits with code 1
result = subprocess.run(
    [sys.executable, "-c", "import sys; print('oops', file=sys.stderr); sys.exit(1)"],
    capture_output=True,
    text=True,
)
print(result.returncode)     # 1
print(result.stderr.strip()) # oops

# check=True turns a failure into an exception
try:
    subprocess.run(
        [sys.executable, "-c", "import sys; sys.exit(3)"],
        capture_output=True, text=True, check=True,
    )
except subprocess.CalledProcessError as e:
    print("command failed with code", e.returncode)  # command failed with code 3

Use check=True when a failed command should stop your program — it spares you from forgetting to test .returncode by hand. Catch CalledProcessError if you want to handle the failure gracefully.

import subprocess, sys

def run_python(code):
    return subprocess.run(
        [sys.executable, "-c", code],
        capture_output=True, text=True,
    )

ok = run_python("print('all good')")
print("rc:", ok.returncode, "| out:", ok.stdout.strip())

# Now a failing command, handled via check=True
try:
    subprocess.run(
        [sys.executable, "-c", "raise SystemExit(2)"],
        capture_output=True, text=True, check=True,
    )
except subprocess.CalledProcessError as e:
    print("failed with code", e.returncode)

# ✅ Expected output:
# rc: 0 | out: all good
# failed with code 2

3 List vs shell=True (the Injection Risk) and timeout

Passing a list is the safe default: each element is one literal argument, so nothing in the data can be reinterpreted as a command. Passing a string with shell=True hands the whole line to a shell, which is where injection attacks happen:

import subprocess

# SAFE: a list of literal arguments
subprocess.run(["echo", user_input])     # user_input is just one argument

# DANGEROUS: a string parsed by the shell
# If user_input is  '; rm -rf ~'  the shell will run BOTH commands!
subprocess.run(f"echo {user_input}", shell=True)   # <-- never do this with untrusted input

And a hung command should never freeze your whole program. Pass timeout (in seconds) and catch TimeoutExpired:

import subprocess, sys

try:
    subprocess.run(
        [sys.executable, "-c", "import time; time.sleep(5)"],
        timeout=1,                # give it one second
        capture_output=True, text=True,
    )
except subprocess.TimeoutExpired:
    print("the command took too long and was killed")
import subprocess, sys

# Safe: arguments as a list, no shell involved
result = subprocess.run(
    [sys.executable, "-c", "for i in range(3): print('line', i)"],
    capture_output=True, text=True,
)
print(result.stdout.splitlines())   # ['line 0', 'line 1', 'line 2']

# timeout protects against a command that never finishes
try:
    subprocess.run(
        [sys.executable, "-c", "import time; time.sleep(5)"],
        timeout=1, capture_output=True, text=True,
    )
except subprocess.TimeoutExpired:
    print("killed: took too long")

# ✅ Expected output:
# ['line 0', 'line 1', 'line 2']
# killed: took too long

🎯 Your Turn: Fill in the Blanks

Complete the call so it captures output as text and reads the result. Replace each ___ and match the expected output.

# 🎯 YOUR TURN: complete the subprocess call
import subprocess, sys

result = subprocess.run(
    [sys.executable, "-c", "print('captured!')"],
    capture_output=True,
    ___=True,                # 1. get strings instead of bytes
)

# 2. 0 means the command succeeded:
print("success:", result.___ == 0)   # success: True
print(result.stdout.strip())         # captured!

# Expected output:
# success: True
# captured!

First blank: text — text=True returns strings instead of bytes.

Second blank: returncode — a value of 0 means the command succeeded.

Common Errors to Avoid

❌ Passing the whole command as one string (no shell)

subprocess.run("echo hello")   # FileNotFoundError: looks for a program literally named "echo hello"

✅ Split it into a list: subprocess.run(["echo", "hello"]).

❌ Forgetting capture_output, then reading stdout

r = subprocess.run(["echo", "hi"])
print(r.stdout)                # None — output went straight to the terminal

✅ Add capture_output=True, text=True to collect the output as a string.

❌ Using shell=True with untrusted input

subprocess.run(f"ls {name}", shell=True)   # command injection if name is hostile

✅ Pass a list: subprocess.run(["ls", name]) — the input can never become a command.

🎯 Mini Challenge: A Tiny Command Runner

Write a helper that runs a snippet of Python and reports whether it succeeded, returning the trimmed output on success and the error text on failure.

import subprocess, sys

def run_snippet(code):
    result = subprocess.run(
        [sys.executable, "-c", code],
        capture_output=True, text=True,
    )
    if result.returncode == 0:
        return ("ok", result.stdout.strip())
    return ("error", result.stderr.strip().splitlines()[-1])

print(run_snippet("print(6 * 7)"))
print(run_snippet("print(undefined_name)"))

# ✅ Expected output:
# ('ok', '42')
# ('error', "NameError: name 'undefined_name' is not defined")

📚 Keep Learning

Go deeper with the official Python documentation:

Frequently Asked Questions

Lesson complete — Python can drive other programs now!

You can run commands with subprocess.run, capture stdout and stderr as text, inspect returncode, fail loudly with check=True, avoid the shell=True injection risk by passing a list, and guard against hangs with timeout.

Practice quiz

What is the modern, recommended function for running external commands?

  • os.system
  • os.popen
  • subprocess.run
  • subprocess.fork

Answer: subprocess.run. subprocess.run is the one safe, flexible function that replaces the older os.system and os.popen.

Which argument makes subprocess.run capture a command's output?

  • capture_output=True
  • stdout=True
  • save=True
  • output=True

Answer: capture_output=True. Pass capture_output=True to collect stdout and stderr on the returned CompletedProcess.

What does text=True do?

  • Runs the command in a text editor
  • Enables colored output
  • Forces UTF-8 only
  • Returns stdout/stderr as strings instead of raw bytes

Answer: Returns stdout/stderr as strings instead of raw bytes. Without text=True, .stdout and .stderr come back as bytes; text=True decodes them to strings.

What does a returncode of 0 mean?

  • The command failed
  • The command succeeded
  • The command is still running
  • No output was produced

Answer: The command succeeded. A returncode of 0 indicates success; any non-zero value means the command failed.

What does check=True do when a command fails?

  • Raises a CalledProcessError automatically
  • Silently retries the command
  • Returns None
  • Prints the error and continues

Answer: Raises a CalledProcessError automatically. check=True turns a non-zero exit into a CalledProcessError, the 'fail loudly' equivalent of an exception.

Why is passing a list of arguments safer than shell=True?

  • Lists run faster
  • shell=True is deprecated
  • Each element is one literal argument, so input can't be reinterpreted as a command
  • Lists support more programs

Answer: Each element is one literal argument, so input can't be reinterpreted as a command. With a list, each item is a literal argument; shell=True parses a string through a shell, enabling command injection.

Which CompletedProcess attribute holds a program's normal output?

  • .stderr
  • .stdout
  • .output
  • .returncode

Answer: .stdout. .stdout holds the standard output, .stderr the error output, and .returncode the exit status.

How do you stop a command that hangs forever?

  • Pass kill=True
  • Call subprocess.stop()
  • Set returncode=1
  • Pass a timeout argument in seconds

Answer: Pass a timeout argument in seconds. Pass timeout=seconds; if the command isn't done by then, Python kills it and raises TimeoutExpired.

What exception is raised when a timeout expires?

  • subprocess.CalledProcessError
  • subprocess.TimeoutExpired
  • TimeoutError only
  • OSError

Answer: subprocess.TimeoutExpired. An expired timeout raises subprocess.TimeoutExpired, which you can catch to handle the stuck process.

What does subprocess.run("echo hello") (a bare string, no shell) do?

  • Prints 'hello' correctly
  • Runs echo with one argument
  • Looks for a program literally named 'echo hello' and raises FileNotFoundError
  • Uses the shell automatically

Answer: Looks for a program literally named 'echo hello' and raises FileNotFoundError. Without shell=True, the whole string is treated as one program name. Split it into a list: ['echo', 'hello'].

Continue this course

Frequently asked questions

What is subprocess used for?

The subprocess module lets your Python program run other programs — command-line tools, scripts, or system utilities — and collect their output. You use it to call git, ffmpeg, a shell command, or any executable, then read what it printed and check whether it succeeded. It replaces the older os.system and os.popen functions with one safe, flexible function: subprocess.run.

What is the difference between passing a list and using shell=True?

When you pass a list like ['echo', 'hi'], Python runs the program directly and treats each list element as one literal argument, so spaces and special characters cannot be misinterpreted. With shell=True you pass a single string that a shell parses, which is convenient but dangerous: if any part of that string came from user input, an attacker could inject extra commands. Prefer the list form and avoid shell=True unless you truly need shell features.

How do I read a command's output and check if it worked?

Call subprocess.run with capture_output=True and text=True. The returned CompletedProcess object has .returncode (0 means success), .stdout (what the program printed to standard output as a string), and .stderr (its error output). Inspect .returncode yourself, or pass check=True to make Python raise a CalledProcessError automatically when the command fails.

How do I stop a command that hangs forever?

Pass a timeout argument, in seconds: subprocess.run(cmd, timeout=5). If the command has not finished by then, Python kills it and raises subprocess.TimeoutExpired, which you can catch. This protects your program from waiting forever on a process that is stuck or producing no output.