subprocess starts external programs and connects their input, output, and return status. Crossing from Python into the operating system requires controlled arguments, time, and environment.
import subprocess
result = subprocess.run(
["git", "rev-parse", "--short", "HEAD"],
check=True,
capture_output=True,
text=True,
timeout=10,
)
commit = result.stdout.strip()
An argument list avoids automatic shell interpretation. check=True raises on failure, timeout limits waiting, and text=True decodes output.
Never concatenate user input into a command with shell=True; metacharacters can inject more commands. Validate against an allowlist and pass values as separate items. Windows batch files have platform-specific parsing behavior, so review the official security notes.
Set cwd explicitly when relative paths matter. Provide a controlled env and keep secrets out of command logs. For large output, use Popen.communicate() or files rather than manual pipe reads that can deadlock.
The Bandit security analysis guide can detect suspicious patterns, but review must still trace argument origins.
The official subprocess documentation, accessed July 22, 2026, recommends run() for supported cases and details timeouts, pipes, and security. Test failure, timeout, and missing-executable paths too.
Treat the executable and its arguments separately
With the default shell=False, Python starts the named program directly. Each list item becomes one argument, so spaces inside a filename do not split it and shell metacharacters do not gain special meaning. Do not build the list with command.split(): quoting rules differ by platform and a filename can legitimately contain spaces. Construct the list from known pieces.
Validation still matters because a safely separated argument may change the target program's behavior. A user value beginning with - can be interpreted as an option, and an unrestricted path can expose an unintended file. Prefer an allowlist or map public choices to internal arguments:
FORMAT_ARGS = {
"short": ["--format", "short"],
"json": ["--format", "json"],
}
def report(format_name: str) -> str:
try:
extra = FORMAT_ARGS[format_name]
except KeyError as exc:
raise ValueError("unsupported format") from exc
result = subprocess.run(
["/opt/acme/bin/report", *extra],
check=True,
capture_output=True,
text=True,
timeout=15,
)
return result.stdout
An absolute executable path avoids depending on the current PATH. When command discovery is intentional, shutil.which() can resolve the program once and your application can verify the result.
Handle every expected failure
check=True raises CalledProcessError for a nonzero status. A missing executable raises FileNotFoundError; an expired limit raises TimeoutExpired. Catch only errors you can translate into a meaningful application outcome:
try:
completed = subprocess.run(
["git", "status", "--porcelain=v1"],
check=True,
capture_output=True,
text=True,
timeout=5,
)
except subprocess.TimeoutExpired as exc:
raise RuntimeError("git did not finish in time") from exc
except subprocess.CalledProcessError as exc:
detail = (exc.stderr or "").strip()
raise RuntimeError(f"git failed with status {exc.returncode}") from exc
except FileNotFoundError as exc:
raise RuntimeError("git is not installed") from exc
Avoid returning raw standard error to a browser because it can contain paths, environment details, or tokens. Log a redacted diagnostic under access controls and give the caller a stable message. A nonzero status is program-specific: some tools use it for “differences found,” so consult that tool's contract before enabling check=True.
Bound time, output, and input
timeout limits how long Python waits, but process creation itself may not be interruptible on every platform. On expiration, run() kills the child, waits for it, and raises TimeoutExpired. Test this behavior with the real tool, especially when that tool launches descendants.
capture_output=True retains both streams in memory. It is convenient for small responses, not backups or video encoders. Redirect predictable large output to an already opened file:
from pathlib import Path
destination = Path("/var/lib/acme/report.json")
with destination.open("wb") as output:
subprocess.run(
["/opt/acme/bin/report", "--json"],
stdout=output,
stderr=subprocess.PIPE,
check=True,
timeout=60,
)
For interactive or incremental processing, use Popen, but keep backpressure in mind. Reading stdout while ignoring a filled stderr pipe can deadlock. communicate() reads both streams safely and should be preferred unless a carefully tested streaming design is required. Never use unlimited external input, runtime, or captured output in a request handler.
Pass input with input= rather than embedding it in the command line. Process listings and audit logs often expose arguments:
result = subprocess.run(
["/usr/bin/tool", "--read-stdin"],
input=payload,
text=True,
capture_output=True,
check=True,
timeout=10,
)
Do not pass secrets if the program offers a protected file descriptor, credential store, or environment-specific secret facility. Environment variables are less visible than arguments but are not a universal secret boundary.
Control working directory and environment
Relative paths depend on cwd; inherited behavior depends on env. Set cwd to a trusted resolved directory when the child expects project files. Never let untrusted input select an arbitrary working directory.
Passing env replaces the complete child environment. Start from os.environ.copy() when the program needs normal operating-system variables, then override only intended keys. For higher isolation, construct a minimal environment and include the exact variables the executable requires. Remove dangerous language or loader variables when starting privileged tooling, and never log the resulting mapping.
Use encoding="utf-8" and errors="strict" when the tool promises UTF-8. text=True otherwise uses the locale's default encoding, which can vary between developer machines and production. Keep binary mode for arbitrary bytes.
Use a shell only for shell syntax
Pipelines, redirection, wildcard expansion, and shell built-ins are reasons a shell might be required. Prefer implementing simple file redirection with stdin and stdout, and pipelines with multiple Popen objects. If a shell truly is necessary, keep the command constant and prevent external data from entering it. shlex.quote() targets POSIX shells and is not a portable sanitizer for Windows command processors.
On Windows, batch files can involve operating-system shell parsing even when shell=False; follow the platform security note in the Python documentation and test arguments containing spaces and metacharacters. Do not assume behavior observed on Linux transfers unchanged.
Manage long-running children
Popen is appropriate when the application must stream data, send signals, or manage a service-like child. Use it as a context manager so file descriptors close, and call communicate() to reap the process. Define shutdown behavior and process-group handling for children that spawn descendants. Avoid launching detached background work from a web request unless a supervised job system owns lifecycle, retries, logs, and resource limits.
Tests should cover success, documented nonzero statuses, missing executable, invalid encoding, timeout, large stderr, filenames with spaces, and hostile-looking argument text. Assert that untrusted text remains one argument. Mocking is useful for application branches, but at least one integration test should execute the actual supported program because option and platform behavior cannot be inferred from subprocess alone.
The safe default is a fixed executable, an explicit list of validated arguments, shell=False, bounded time and output, a deliberate environment, and handled return status. These controls make external processes predictable without pretending that argument separation validates the program's own semantics.
Before deployment, also verify filesystem permissions and the operating-system account used by the child. subprocess does not create a sandbox: the program inherits the parent's effective capabilities unless the surrounding service restricts them. Run the application with least privilege, keep writable directories narrow, and use operating-system resource controls for CPU, memory, and process counts when executing costly tools.