Need to run shell commands from Python? Here's how to do it safely.
The Basics
import subprocess
# Simple command
result = subprocess.run(["ls", "-la"])
# Capture output
result = subprocess.run(
["ls", "-la"],
capture_output=True,
text=True
)
print(result.stdout)subprocess.run()
The main function for running commands:
result = subprocess.run(
["git", "status"],
capture_output=True, # Capture stdout and stderr
text=True, # Return strings, not bytes
check=True, # Raise on non-zero exit
timeout=30, # Timeout in seconds
cwd="/path/to/repo", # Working directory
env={"PATH": "/usr/bin"}, # Environment variables
)
print(result.returncode) # Exit code
print(result.stdout) # Standard output
print(result.stderr) # Standard errorChecking Exit Codes
# Method 1: check=True raises CalledProcessError
try:
subprocess.run(["false"], check=True)
except subprocess.CalledProcessError as e:
print(f"Command failed with code {e.returncode}")
# Method 2: Check manually
result = subprocess.run(["ls", "nonexistent"])
if result.returncode != 0:
print("Command failed")Capturing Output
# Capture both stdout and stderr
result = subprocess.run(
["git", "log", "-1"],
capture_output=True,
text=True
)
# Or explicitly
result = subprocess.run(
["git", "log", "-1"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
# Merge stderr into stdout
result = subprocess.run(
["command"],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True
)Providing Input
# String input
result = subprocess.run(
["grep", "error"],
input="line1\nerror here\nline3",
capture_output=True,
text=True
)
print(result.stdout) # "error here"
# From file
with open("input.txt") as f:
result = subprocess.run(["wc", "-l"], stdin=f)Shell Commands
# Avoid shell=True when possible
subprocess.run(["ls", "-la"]) # Safe
# When you need shell features
subprocess.run("ls -la | grep py", shell=True) # Risky
# Safer shell usage
subprocess.run(
"ls -la | grep py",
shell=True,
check=True,
capture_output=True
)Never use shell=True with user input — command injection risk.
Piping Between Commands
# Using shell
subprocess.run("cat file.txt | grep error | wc -l", shell=True)
# Without shell (safer)
p1 = subprocess.Popen(
["cat", "file.txt"],
stdout=subprocess.PIPE
)
p2 = subprocess.Popen(
["grep", "error"],
stdin=p1.stdout,
stdout=subprocess.PIPE
)
p1.stdout.close()
output = p2.communicate()[0]Timeouts
try:
result = subprocess.run(
["sleep", "10"],
timeout=5
)
except subprocess.TimeoutExpired:
print("Command timed out")Working Directory
# Run in specific directory
result = subprocess.run(
["git", "status"],
cwd="/path/to/repo",
capture_output=True,
text=True
)Environment Variables
import os
# Add to existing environment
env = os.environ.copy()
env["MY_VAR"] = "value"
subprocess.run(["command"], env=env)
# Replace environment entirely
subprocess.run(
["command"],
env={"PATH": "/usr/bin", "HOME": "/tmp"}
)Common Patterns
Run and get output
def run_command(cmd: list[str]) -> str:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
check=True
)
return result.stdout.strip()
version = run_command(["python", "--version"])Run with error handling
def run_safe(cmd: list[str]) -> tuple[bool, str]:
try:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
check=True
)
return True, result.stdout
except subprocess.CalledProcessError as e:
return False, e.stderrMy Rules
- Use lists, not strings —
["ls", "-la"]not"ls -la" - Avoid shell=True — unless you need shell features
- Always set timeout — prevent hung processes
- Use text=True — unless you need bytes
- Check return codes — don't assume success
Subprocess is powerful. Use it carefully.
React to this post: