Python subprocess arguments disappear with shell=True: check the POSIX boundary
By ON Lab · Sources checked November 8, 2026
On POSIX, a sequence passed with shell=True is not the same argument list you would pass directly to a program. Its first item becomes the shell command text; later items become arguments to that shell invocation. For a known executable and separate data values, start with shell=False and one list item per argument. A value containing spaces stays one item.
A historical reader reported a Bash script receiving no expected parameters after calling subprocess with a list and shell=True. A second reader asked why a shell-enabled list seemed to work for a directory listing. We checked the current Python documentation and reproduced the argument boundary on macOS with Python 3.9.6. This is a POSIX explanation, not a Windows cmd.exe or PowerShell reproduction.
First inspect what the child actually receives
For a disposable Python receiver, save the following as show_arguments.py in a new working directory:
import json
import sys
print(json.dumps(sys.argv[1:]))
Call it through the intended Python executable:
import subprocess
import sys
result = subprocess.run(
[sys.executable, "show_arguments.py", "alpha beta", "gamma"],
capture_output=True,
text=True,
check=True,
)
print(result.stdout)
Our equivalent local test printed a JSON array with exactly two elements: ["alpha beta", "gamma"]. The space inside the first value did not create another argument. List boundaries, rather than shell quotes embedded in each value, expressed the intended separation.
Run this from the directory containing the receiver or use its verified absolute path. sys.executable selects the current Python executable; it is not a substitute for finding a different program's intended installation. Keep paths and each data argument as separate list elements.
Why enabling the shell changed the script call
The official documentation describes a POSIX shell invocation in which the first sequence item supplies the command and the remaining items go to the shell. That shell is an intermediate process. It then parses its command text to decide what program to run and what arguments that program receives.
Consequently, adding shell=True to an existing executable-and-arguments list can move values away from the target program's argument list. A successful process launch does not prove the child received the same values. A directory listing can appear plausible even when an expected option did not reach the listing command.
In our harmless shell fixture, a script printed its argument count and its first two values. The shell-enabled sequence placed the script invocation entirely in the first element and put two values in later elements. The script printed a count of zero. Calling /bin/sh directly with the script path and those two values printed a count of two and preserved the embedded space.
Use an explicit script interpreter when appropriate
subprocess.run(
["/bin/sh", "/path/to/arguments.sh", "alpha beta", "gamma"],
check=True,
)
This starts the named shell as the executable with ordinary arguments, while Python's shell option stays at its default false value. The script itself is still a shell script and can interpret its own contents. These are different boundaries: selecting an interpreter to run a trusted script is not the same operation as asking Python to interpret a constructed command string.
Choose the interpreter the script actually requires. A script using Bash-specific features needs an appropriate Bash invocation; our fixture deliberately used only portable printf and positional parameters. We did not establish that every Bash script runs under /bin/sh.
Separate arguments from command-language features
| Intention | Starting approach |
|---|---|
| Run an executable with data values | Sequence with separate values and shell=False |
| Run a trusted script | Its documented executable or interpreter and separate arguments |
| Redirect output to a file | Consider subprocess stdout with an opened file |
| Run a shell-language expression | Use the chosen shell deliberately and review its parsing rules |
A pipe character or wildcard in a direct argument list is passed as data; Python does not automatically apply shell expansion to it. Conversely, joining arbitrary values into shell command text adds a parsing step. Review the official security guidance if shell interpretation is actually required. An argument list avoids that intermediate shell parsing, but the target program still assigns meaning to options and values.
Read the result at each boundary
- Record the operating system, Python version and executable path.
- Write down the exact intended argument values without combining them into one string.
- Use a small receiver or a script's own diagnostic mode to inspect received values.
- Check the return code and captured output for the intended operation.
- Only add shell features that the task actually requires, then repeat the receiver check.
If the direct receiver is correct but the real command still fails, investigate that program's own syntax, current working directory, environment and exit message. Quoting advice for a different shell is not evidence that a missing file or program-specific option has been repaired.
Checked November 8, 2026. The local argument and count tests were actually executed. Windows process argument conversion and batch-file behavior have separate documented rules and were not tested here. The old reader questions establish a concrete problem, not current traffic or a universal fix.