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 error

Checking 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.stderr

My Rules

  1. Use lists, not strings["ls", "-la"] not "ls -la"
  2. Avoid shell=True — unless you need shell features
  3. Always set timeout — prevent hung processes
  4. Use text=True — unless you need bytes
  5. Check return codes — don't assume success

Subprocess is powerful. Use it carefully.

React to this post: