Difficulty: Intermediate
How do you run external commands in Python with the subprocess module?
subprocess (Python 3.5+) is the modern way to run external programs, replacing os.system() and os.popen().
subprocess.run(): high-level API for simple command execution. Waits for completion. subprocess.Popen(): low-level API for streaming output, async execution.
Security: never use shell=True with user input - it enables shell injection. Pass arguments as a list instead.
Key parameters: - check=True: raises CalledProcessError on non-zero exit - capture_output=True: captures stdout and stderr - text=True: decode bytes to string (encoding=sys.stdout.encoding)
import subprocess
import sys
# Basic run - wait for completion
result = subprocess.run(
['ls', '-la'],
capture_output=True,
text=True,
check=True # Raise on non-zero exit
)
print(result.stdout)
print('Return code:', result.returncode)
# Handle errors
try:
subprocess.run(
['git', 'status'],
check=True,
capture_output=True,
text=True
)
except subprocess.CalledProcessError as e:
print(f'Command failed: {e.returncode}')
print(f'Stderr: {e.stderr}')
except FileNotFoundError:
print('git not installed')
# Timeout
try:
subprocess.run(['sleep', '10'], timeout=2, check=True)
except subprocess.TimeoutExpired:
print('Command timed out')
# NEVER do this with user input:
user_input = 'safe.txt; rm -rf /'
# subprocess.run(f'cat {user_input}', shell=True) # SHELL INJECTION!
# SAFE: pass as list
subprocess.run(['cat', 'safe.txt'], check=True) # Arguments are not interpreted by shell
check=True turns non-zero exit codes into exceptions. capture_output=True = stdout=PIPE, stderr=PIPE. Lists prevent shell injection.
import subprocess
# Popen for streaming output
with subprocess.Popen(
['ping', '-c', '4', 'google.com'],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True
) as proc:
for line in proc.stdout:
print(line, end='') # Stream line by line
# Communicate: send stdin, get stdout/stderr
result = subprocess.run(
['python', '-c', 'import sys; print(sys.stdin.read().upper())'],
input='hello world',
capture_output=True,
text=True
)
print(result.stdout) # HELLO WORLD\n
# Run Python subprocess (same Python executable)
result = subprocess.run(
[sys.executable, '-c', 'print(1 + 1)'],
capture_output=True,
text=True
)
print(result.stdout) # 2
# Check if command exists
def is_installed(command):
return subprocess.run(
['which', command],
capture_output=True
).returncode == 0
print(is_installed('git')) # True or False
print(is_installed('foo')) # False
Popen enables line-by-line streaming without buffering all output in memory. sys.executable ensures the same Python version is used for subprocesses.
subprocess, run(), Popen, check=True, Shell Injection