subprocess is a standard library module for spawning new processes, connecting to their input/output/error pipes, and obtaining their return codes. subprocess.run() is the recommended high-level API for most use cases. It replaces older functions like os.system() and os.popen().
Running external programs from Python
What you'll learn: How to run external commands with subprocess.run(), check exit codes, and capture a command's output into your Python program.
How to read this tab: Try subprocess.run(["echo", "hello"]) first. Then add capture_output=True, text=True and inspect result.stdout. Build up from running a command to capturing its output.
subprocess lets your Python program run other programs — like running a terminal command from inside your code. Want to run git status, call ffmpeg, or execute a shell script? subprocess is how. subprocess.run() is the one function to learn first.
Pass arguments as a list, not a string. subprocess.run(["ls", "-l", "/home"]) — each argument is a separate list item. subprocess.run("ls -l /home") looks for a program literally named "ls -l /home" and fails. The list form is also injection-safe. This is the most common subprocess mistake.
subprocess.run() — the main API
The subprocess.run() function (Python 3.5+) is the recommended way to run external commands. Pass the command as a list of strings — the program name first, then each argument as a separate list item.
import subprocess
# Run a command — arguments as a list
result = subprocess.run(["echo", "Hello from subprocess"])
# Hello from subprocess (printed to terminal)
# Check the exit code
result = subprocess.run(["ls", "/tmp"])
print(result.returncode) # 0 means success, non-zero means error
# Run git status (in a git repository)
result = subprocess.run(["git", "status"])
# IMPORTANT: pass arguments as separate list items, NOT one string
# RIGHT:
subprocess.run(["ls", "-l", "/home"])
# WRONG (unless shell=True):
# subprocess.run("ls -l /home") <- this looks for a program
# literally named "ls -l /home"
# Raise an exception if the command fails (check=True)
try:
subprocess.run(["false"], check=True) # 'false' exits with code 1
except subprocess.CalledProcessError as e:
print(f"Command failed with code {e.returncode}")capture_output=True, text=True is the combination you want. capture_output captures stdout and stderr into the result object. text=True decodes them from bytes to strings (otherwise you get b"output"). Without these, output goes straight to the terminal and you can't process it in your program.
Capturing output
By default, a subprocess's output goes to the terminal. To capture it in your Python program, pass capture_output=True and text=True.
import subprocess
# Capture stdout as text
result = subprocess.run(
["git", "branch", "--show-current"],
capture_output=True, # capture stdout and stderr
text=True, # decode bytes to str (instead of raw bytes)
)
print(f"Current branch: {result.stdout.strip()}")
# Capture and process output
result = subprocess.run(
["ls", "-1", "/usr/bin"],
capture_output=True,
text=True,
)
programs = result.stdout.splitlines()
print(f"Found {len(programs)} programs in /usr/bin")
# stdout and stderr are separate
result = subprocess.run(
["python3", "-c", "import sys; print('out'); print('err', file=sys.stderr)"],
capture_output=True,
text=True,
)
print(f"stdout: {result.stdout}") # out
print(f"stderr: {result.stderr}") # err
# Get output as a simple string (convenience for capture + check)
output = subprocess.check_output(["date"], text=True)
print(output.strip())✅ Beginner tab complete
- I can run a command with subprocess.run(["program", "arg1", "arg2"])
- I pass arguments as separate list items, NOT as one string
- I can capture output with capture_output=True, text=True
- I can check success with result.returncode or check=True
Input, pipelines, timeouts, and security
What you'll learn: Sending input to a process, building pipelines, setting timeouts and working directories, and the critical security rule about shell=True and command injection.
How to read this tab: Read the security section carefully. The difference between the list form and shell=True is the difference between safe and exploitable code. This is one of the most important security lessons in Python.
Set a timeout to avoid hung processes. subprocess.run(["command"], timeout=30) raises TimeoutExpired if the command runs longer than 30 seconds. Without a timeout, a hung external program hangs your entire Python program indefinitely. For any command that talks to a network or external system, set a timeout.
Sending input and building pipelines
You can send data to a program's standard input via the input parameter, set a timeout, and even chain programs together as a pipeline.
import subprocess
# Send input to a program's stdin
result = subprocess.run(
["grep", "error"],
input="line 1\nerror line\nline 3\n", # fed to stdin
capture_output=True,
text=True,
)
print(result.stdout) # error line
# Set a timeout — kill the process if it runs too long
try:
subprocess.run(["sleep", "10"], timeout=2)
except subprocess.TimeoutExpired:
print("Command timed out after 2 seconds")
# Set the working directory and environment
result = subprocess.run(
["pwd"],
cwd="/tmp", # run in this directory
capture_output=True, text=True,
)
print(result.stdout.strip()) # /tmp
import os
custom_env = {**os.environ, "MY_VAR": "value"}
subprocess.run(["printenv", "MY_VAR"], env=custom_env)
# Pipelines — connect one program's output to another's input
# Equivalent to: ls /usr/bin | grep python
ls = subprocess.run(["ls", "/usr/bin"], capture_output=True, text=True)
grep = subprocess.run(
["grep", "python"],
input=ls.stdout, # feed ls output into grep
capture_output=True, text=True,
)
print(grep.stdout)
# For true streaming pipelines, use Popen
p1 = subprocess.Popen(["ls", "/usr/bin"], stdout=subprocess.PIPE)
p2 = subprocess.Popen(["grep", "python"], stdin=p1.stdout,
stdout=subprocess.PIPE, text=True)
p1.stdout.close() # allow p1 to receive SIGPIPE if p2 exits
output, _ = p2.communicate()
print(output)shell=True with user input is a command injection vulnerability. subprocess.run(f"cat {filename}", shell=True) where filename is user-controlled lets an attacker run x.txt; rm -rf /. The list form subprocess.run(["cat", filename]) treats the entire filename as a single argument — injection-proof. Never use shell=True with any input you don't fully control.
Security: shell=True and command injection
The most important subprocess safety rule: avoid shell=True with untrusted input. It opens your program to command injection attacks.
import subprocess
# DANGEROUS — command injection vulnerability
filename = input("Enter filename: ") # user types: x.txt; rm -rf /
subprocess.run(f"cat {filename}", shell=True) # NEVER DO THIS
# The shell interprets ; rm -rf / as a second command!
# SAFE — arguments as a list, no shell
filename = input("Enter filename: ")
subprocess.run(["cat", filename]) # filename is a single argument
# Even "x.txt; rm -rf /" is treated as one (invalid) filename — safe
# If you MUST use shell features (pipes, globbing, redirection in the
# shell), sanitise input with shlex.quote — but prefer the list form:
import shlex
safe_arg = shlex.quote(filename)
# The list form also handles spaces and special characters correctly:
subprocess.run(["cat", "file with spaces.txt"]) # works perfectly
# vs shell=True where you'd need careful quoting✅ Intermediate tab complete
- I can send input to a process with the input= parameter
- I can set a timeout= to kill a hung process
- I NEVER use shell=True with untrusted input (command injection risk)
- I use the list form ["cmd", arg] which is injection-safe by default
Popen, the process lifecycle, and async subprocess
What you'll learn: The Popen class for fine-grained process control, streaming output as it's produced, poll() vs wait(), communicate() for deadlock-free pipe handling, and asyncio.create_subprocess_exec for concurrent processes.
How to read this tab: Use Popen to stream the output of a long-running command (like ping) line by line as it's produced, rather than waiting for it to finish.
Popen, process lifecycle, and async subprocess
Under the hood, subprocess.run() is a convenience wrapper around the Popen class, which provides fine-grained control over process creation and the process lifecycle. Use Popen directly when you need to interact with a long-running process, stream output as it's produced, or manage multiple processes concurrently.
import subprocess
# Popen — start a process and interact with it while it runs
proc = subprocess.Popen(
["ping", "-c", "5", "example.com"],
stdout=subprocess.PIPE,
text=True,
)
# Stream output line by line as the process produces it
for line in proc.stdout:
print(f"Got: {line.strip()}")
proc.wait() # wait for the process to finish
print(f"Exit code: {proc.returncode}")
# poll() — check if the process is done without blocking
import time
proc = subprocess.Popen(["sleep", "3"])
while proc.poll() is None: # None means still running
print("Still running...")
time.sleep(1)
print("Done")
# Async subprocess with asyncio
import asyncio
async def run_command(cmd):
proc = await asyncio.create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stdout, stderr = await proc.communicate()
return stdout.decode()
async def main():
# Run multiple commands concurrently
results = await asyncio.gather(
run_command(["echo", "first"]),
run_command(["echo", "second"]),
run_command(["echo", "third"]),
)
print(results)
asyncio.run(main())The process creation mechanism differs by platform: on POSIX systems, subprocess uses fork() followed by exec() (or the more efficient posix_spawn() where available); on Windows it uses CreateProcess(). The communicate() method is the safe way to interact with a process's pipes — it reads stdout and stderr concurrently, avoiding the deadlock that occurs if you naively read one pipe while the process blocks writing to the other.
✅ Expert tab complete
- I can use Popen to start a process and stream its output while it runs
- I know communicate() reads stdout and stderr concurrently to avoid deadlock
- I can run subprocesses concurrently with asyncio.create_subprocess_exec