Running Subprocesses Safely
Use subprocess.run for commands that start, finish, and return a bounded result:
import subprocess
completed = subprocess.run(
["git", "status", "--short"],
check=True,
capture_output=True,
text=True,
encoding="utf-8",
timeout=30,
)
print(completed.stdout)
Pass the executable and each argument as separate list elements. Without a shell, spaces and shell metacharacters in an argument remain part of that argument.
Result and failure contracts
check=TrueraisesCalledProcessErrorfor a nonzero exit status.capture_output=Truecaptures both output streams; without text mode they are bytes.timeout=bounds communication after process creation. If it expires,run()terminates and waits for the child before raisingTimeoutExpired, but some platform process-creation delays are not interruptible.- Capturing unbounded output can exhaust memory. Inherit streams, redirect to a file, or consume incrementally when volume is unknown.
Treat expected nonzero statuses explicitly instead of setting check=False and
forgetting to inspect returncode.
Environment and working directory
import os
from pathlib import Path
child_env = os.environ.copy()
child_env["APP_MODE"] = "batch"
subprocess.run(
["tool", "--input", "data.json"],
cwd=Path("workspace"),
env=child_env,
check=True,
)
An env mapping replaces the child's environment rather than adding to it.
Start from a copy only when inheritance is intended. A controlled minimal
environment reduces hidden dependencies; an inherited one may carry credentials
and influence executable or library lookup.
Resolve the executable deliberately for privileged or security-sensitive code.
Do not assume the caller's PATH is trustworthy.
Shell boundary
Use shell=True only when shell language features are the actual requirement,
not as a shortcut for building an argument string. With a shell, quoting,
expansion, pipelines, redirection, and injection become part of the contract.
Never concatenate untrusted text into a shell command. Platform rules differ,
especially for Windows batch files.
For long-lived or interactive children, use Popen and own the complete
lifecycle: pipe drainage, shutdown, signals, timeouts, and cleanup. Use
asyncio.create_subprocess_exec when asynchronous orchestration is genuinely
needed. Calling wait() while a child can fill an unread pipe may deadlock;
communicate() coordinates common pipe use cases.