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
- Running a command with subprocess.run
- Capturing stdout and stderr as text
- Reading returncode to detect success or failure
- Failing loudly with check=True
- Why a list of args is safer than shell=True
- Stopping a hung command with timeout
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()) # 4import 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: True2 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 3Use 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 23 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 inputAnd 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:
- subprocess — Subprocess management
- subprocess.run and CompletedProcess reference
- Security considerations (shell=True and injection)
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
- Previous: Randomness with the random Module
- Next: Hashing with hashlib — Compute SHA-256/HMAC digests, hash files, and understand salts vs encryption
- Quick reference: Python cheat sheet
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.