A command line is an argument vector before it is a shell string
Passing subprocess arguments as a sequence preserves argument boundaries and avoids shell parsing by default. It prevents shell metacharacters from becoming syntax, while leaving program-specific option handling as a separate concern.
Processes receive arguments, shells parse strings
A process launch is best understood as a program plus an argument vector. A shell command line is a string that a shell parses into that vector, with rules for spaces, quotes, substitutions, pipes, and redirections. Python’s subprocess.run accepts a sequence of arguments and uses shell=False by default; the documentation recommends a sequence because it can handle quoting and escaping needed for arguments such as filenames containing spaces. [1]
When application data is placed in one list element, a semicolon in that value is just a character passed to the child. It does not become shell syntax because no shell is asked to parse the string. This is a boundary guarantee, not a universal input policy: the target program may interpret an argument beginning with a dash as an option, or may have its own mini-language.
Read the example as a process trace
The example starts the current Python interpreter through sys.executable. Its -c argument tells that interpreter to print the representation of its first user argument. The final list element contains spaces and a semicolon, so the child receives it as one argument. The parent captures standard output and error in text mode, requires a zero exit status, and imposes a five-second timeout. It strips the output’s trailing whitespace before printing it. The resulting representation shows one string, a name; with spaces, rather than a second command being launched. [1]
The example assumes a working Python interpreter and that process creation completes promptly enough for the timeout to apply. The timeout governs waiting for the child; process creation itself cannot always be interrupted immediately on every platform. check=True turns a nonzero child exit into CalledProcessError, while captured streams remain available for diagnosis. text=True decodes streams using the selected or default text encoding behavior.
import subprocess
import sys
result = subprocess.run(
[sys.executable, "-c", "import sys; print(repr(sys.argv[1]))",
"a name; with spaces"],
check=True, capture_output=True, text=True, timeout=5,
)
print(result.stdout.strip()) # 'a name; with spaces'
Option parsing is another boundary
Suppose a program expects a filename but treats --delete-all as an option. Supplying that value as a single argument prevents shell injection, yet the program may still interpret it as a destructive flag. That is option injection: the shell boundary is intact, but the callee’s argument grammar assigns special meaning to the value. Validate the value for the intended domain, and where supported, use an end-of-options marker before operands. The marker is a convention of the target program, not a feature imposed by Python. [1]
Executable selection also deserves deliberate handling. The example uses sys.executable, which the documentation recommends when relaunching the current interpreter. For other programs, an absolute path can avoid platform differences in executable lookup; alternatively, resolve a trusted executable path deliberately. Avoid switching to shell=True merely to express ordinary argument lists. Shell features such as pipes or wildcard expansion require shell parsing, and can often be composed with Python APIs instead.
Choose output and failure behavior explicitly
Captured output is convenient for a short command whose output is bounded. A child that writes a very large amount can consume memory when both streams are captured; for streaming or large output, direct files or a managed Popen flow may fit better. A timeout and check=True answer distinct questions: how long to wait and whether a nonzero exit should be treated as failure. Encoding behavior matters when output is text, while binary mode preserves bytes for protocols or exact data handling. [1]
The design choice is easiest to review when the call site visibly separates executable, fixed options, and data operands. That structure makes argument boundaries inspectable and limits accidental shell interpretation. It still leaves the programmer responsible for selecting the right executable, validating inputs according to the invoked program, and deciding how to handle errors and output.