subprocess & Shell Interop
subprocess runs external binaries from Python with controlled arguments, timeouts, and captured output. Avoid shell=True unless you accept injection risk and quoting complexity.
Search across all documentation pages
subprocess runs external binaries from Python with controlled arguments, timeouts, and captured output. Avoid shell=True unless you accept injection risk and quoting complexity.
import subprocess
proc = subprocess.run(
["git", "rev-parse", "HEAD"],
check=True,
capture_output=True,
text=True,
timeout=30,
)
print(proc.stdout.strip())When to reach for this:
Run command with timeout, map errors to exit codes, stream large output line by line.
import logging
import subprocess
from collections.abc import Sequence
log = logging.getLogger(__name__)
def run_command(cmd: Sequence[str], timeout: float = 60) -> subprocess.CompletedProcess[str]:
log.info("exec %s", list(cmd))
try:
return subprocess.run(
cmd,
check=True,
capture_output=True,
text=True,
timeout=timeout,
)
except subprocess.TimeoutExpired as exc:
log.error("timeout after %ss: %s", timeout, cmd)
raise
except subprocess.CalledProcessError as exc:
log.error("exit %s stderr=%s", exc.returncode, exc.stderr.strip())
raise
def stream_lines(cmd: Sequence[str]) -> int:
with subprocess.Popen(cmd, stdout=subprocess.PIPE, text=True) as proc:
assert proc.stdout is not None
for line in proc.stdout:
print(line.rstrip())
return proc.wait()
if __name__ == "__main__":
result = run_command(["python3", "--version"])
print(result.stdout)What this demonstrates:
check=True raises CalledProcessError with returncode and stderrPopen streams stdout without buffering entire output in memory| API | Use |
|---|---|
run() | Most commands - simple sync |
Popen() | Streaming, pipelines, interactive |
check_output() | Quick capture when check=True always |
shlex.quote on user segmentsimport shlex
user_file = "report; rm -rf /"
# BAD: subprocess.run(f"cat {user_file}", shell=True)
subprocess.run(["cat", user_file], check=True)shlex.split on trusted scripts only.timeout proportional to expected runtime.Popen or redirect to temp file.stderr on non-zero exit.text=False and decode explicitly when needed.| Alternative | Use When | Don't Use When |
|---|---|---|
| Pure Python libs | Operation has mature Python API | Battle-tested CLI already exists |
asyncio.create_subprocess_exec | Async event loop orchestration | Simple sync cron script |
| Fabric/SSH remote | Remote hosts | Local-only command |
Fixed internal scripts with no user input on trusted machines - still document exception and ticket.
env={**os.environ, "FOO": "bar"} - do not dump secrets into child env unnecessarily.
cwd="/path/to/repo" on run() - paths in cmd should be absolute or relative to cwd.
Chain two Popen with stdout->stdin pipe, or use shell pipeline with extreme caution and fixed commands.
Non-zero - map known codes in script docs (e.g. 2 usage error).
Pass via env var or stdin - argv visible in ps on many systems.
.cmd files may need shell=True on Windows only - isolate platform branches.
Mock subprocess.run or use known echo binaries in pytest with tmp_path.
stderr=subprocess.STDOUT when single stream simplifies parsing.
Local subprocess on remote host via SSH session - see SSH page for remote execution patterns.
Stack versions: This page was written for Python 3.14.0 (stable 3.14, maintenance 3.13), FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, and uv 0.6+.
Reviewed by Chris St. John·Last updated Jul 19, 2026