Run Shell Commands in Python Safely with subprocess

Quick answer: Use subprocess.run with an argument list, an explicit timeout, and deliberate output and error handling to run a shell command from Python. Avoid shell=True for untrusted input and treat command execution as a privilege and resource boundary.

Python Pool infographic showing subprocess running a command with an argument list, captured output, timeout, and checked return code
subprocess makes command boundaries explicit; prefer argument lists, capture output intentionally, enforce timeouts, and avoid shell interpretation for untrusted input.

The safest modern way to run shell commands from Python is the subprocess module. It can start another program, pass arguments, capture output, check exit codes, and stop commands that run too long.

When a child process inherits the terminal, application output stays attached to that terminal. For a concrete GUI scripting case, see where the Blender Python output window sends print() messages and tracebacks. For interactive programs that redraw their own display, use the cross-platform Python console clear guide without confusing display cleanup with process state.

Prefer passing command arguments as a list. That avoids shell parsing surprises and makes spaces in file paths easier to handle. Use shell=True only when you truly need shell features such as pipes, wildcards, or shell built-ins.

Running external commands is powerful, but it crosses a boundary from Python into the operating system. Treat command construction as input handling: keep arguments explicit, validate anything that comes from users, and check the exit status.

The official subprocess documentation covers the API. For related process control, see the Python subprocess terminate guide and the Python help function guide.

Run A Command With subprocess.run()

subprocess.run() starts a command and waits for it to finish.

import subprocess
import sys

result = subprocess.run([sys.executable, "--version"], text=True, capture_output=True)

print(result.returncode)
print(result.stdout.strip())

The returned object contains the exit code, standard output, and standard error when output capture is enabled.

If you do not need output, omit capture_output=True and let the child process inherit the terminal streams. Capturing is best when Python needs to inspect or store the result.

Pass Arguments As A List

Each argument should be its own list item. Do not combine a command and its arguments into one long string unless shell parsing is required.

import subprocess
import sys

path = "example file.txt"
command = [
    sys.executable,
    "-c",
    "import sys; print(len(sys.argv[1]))",
    path,
]
result = subprocess.run(command, text=True, capture_output=True)

print(result.stdout.strip())

This style handles spaces in arguments more predictably than building a shell string by hand.

It also avoids quoting rules that differ between shells. Python passes each list item as a separate argument to the child program.

Raise An Error For Failed Commands

Pass check=True when a nonzero exit code should raise an exception.

import subprocess
import sys

try:
    subprocess.run([sys.executable, "-c", "raise SystemExit(2)"], check=True)
except subprocess.CalledProcessError as error:
    print(error.returncode)

This is useful in scripts where a failed external command should stop the workflow immediately.

The exception gives you the return code and, when captured, the output streams. That makes failures easier to log with enough detail for debugging.

Python Pool infographic showing Python, subprocess.run, shell command, and completed result
subprocess.run starts a child process and returns a completed-process result.

Capture stdout And stderr

Use capture_output=True and text=True when you want string output instead of bytes.

import subprocess
import sys

command = [
    sys.executable,
    "-c",
    "import sys; print('ok'); print('warn', file=sys.stderr)",
]
result = subprocess.run(command, text=True, capture_output=True)

print(result.stdout.strip())
print(result.stderr.strip())

Captured output is useful for parsing command results, logging diagnostics, and testing wrapper functions.

Use text=True when you want strings. Without it, Python returns bytes, which is useful for binary output but less convenient for ordinary command text.

Set A Timeout

Use timeout to stop a command that runs too long.

import subprocess
import sys

try:
    subprocess.run(
        [sys.executable, "-c", "from threading import Event; Event().wait(5)"],
        timeout=1,
    )
except subprocess.TimeoutExpired:
    print("command timed out")

Timeouts protect automation jobs from hanging forever when a child process stalls.

Choose a timeout based on the real command. Very short timeouts can make normal slow machines look broken, while no timeout can leave a worker stuck indefinitely.

Use shell=True Carefully

shell=True asks a shell to interpret the command string. That can be useful for shell-only syntax, but it is risky with untrusted input.

import subprocess

result = subprocess.run("printf 'hello\n' | wc -l", shell=True, text=True, capture_output=True)

print(result.stdout.strip())

Never concatenate user-provided text into a shell command. If user input is involved, pass arguments as a list and keep shell=False.

If shell features are required, keep the command string fixed and pass only trusted values. For most application code, a list of arguments is the safer default.

Python Pool infographic mapping a command through capture_output to stdout and stderr
Capture stdout and stderr deliberately when the caller needs to inspect command output.

subprocess Versus os.system

os.system() can run a command, but it gives you much less control. It does not capture output conveniently and does not provide the same structured result object.

Use subprocess.run() for new code. Reach for Popen only when you need streaming communication with a process while it is still running.

Prefer Python APIs When They Fit

Before starting a shell command, check whether Python already has an API for the job. The standard library can list directories, copy files, read text, inspect paths, compress data, and make network requests without leaving the Python process. For shell automation that specifically targets Synology NAS systems, see the command workflow in SynoTools Python Guide for Synology NAS.

Direct Python APIs are usually easier to test and more portable across operating systems. Shell commands are best when you need an existing command-line tool, system integration, or behavior that would be expensive to reimplement.

The reliable pattern is to pass a list of arguments, capture output only when needed, use check=True for required commands, add timeout for automation, and avoid shell=True unless shell parsing is intentional.

Also consider whether Python can do the work directly. File operations, path checks, directory listing, and text processing often have standard-library APIs that are more portable than shell commands.

Python Pool infographic comparing argument list, shell true, untrusted input, and safe process
Prefer an argument list and avoid shell=True with untrusted input.

Pass Arguments As A List

A list such as [“python”, “script.py”, path] lets subprocess pass argument boundaries without a shell re-parsing spaces or metacharacters. Validate and normalize paths before execution.

Use shell=True Only Intentionally

Shell syntax such as pipes and globbing may require a shell, but interpolating untrusted text into a command string can enable injection. Prefer composing subprocess calls or pass a fixed shell script with controlled inputs.

Capture Output With Limits

capture_output=True is convenient for small results, while large or continuous output may need streaming pipes or a file. Decode text explicitly and avoid storing sensitive output in logs.

Python Pool infographic testing timeouts, return codes, environment, signals, and validation
Check timeouts, return codes, environment exposure, signals, logs, and cleanup.

Check Return Codes

Use check=True when any non-zero exit should raise CalledProcessError, or inspect returncode when the command has expected alternative outcomes. Include sanitized stderr in diagnostics.

Set Environment And Working Directory

Pass a minimal environment when reproducibility or security matters, set cwd explicitly, and use an executable path policy. Do not assume the interactive user’s PATH exists in a service.

Handle Timeouts And Cleanup

A timeout prevents a child from blocking forever, but descendants may survive depending on the platform and process tree. Define termination, cleanup, retry, and partial-output behavior for the application.

Use the official Python subprocess documentation for arguments, security, return codes, and timeouts. Related Python Pool references include tests and logging.

For reliable command workflows, compare subprocess tests, safe diagnostics, and argument text handling before executing a shell command.

Frequently Asked Questions

What is the best way to run a command in Python?

Use subprocess.run with a list of arguments and an explicit timeout, then inspect the return code or use check=True when failures should raise.

Why should I avoid shell=True?

shell=True lets a shell interpret the command string and can create injection risk when any part comes from untrusted input; use it only for a documented shell-specific need.

How do I capture command output?

Pass capture_output=True or explicit pipes and choose text=True when decoded strings are desired; handle stderr and output size deliberately.

How do I prevent a command from hanging?

Set a timeout and catch subprocess.TimeoutExpired, then decide whether to terminate, clean up descendants, and report the partial output.

Subscribe
Notify of
guest
0 Comments
Oldest
Newest Most Voted