subprocess shell=True Drops List Args (tested)
subprocess.run(["echo", "hello", "world"], shell=True) looks like it should print hello world. We measured what it actually prints: nothing but a blank line, with exit code 0 and empty stderr — no error at all.
1. A list of args with shell=True silently loses everything but the first item
import subprocess
r = subprocess.run(["echo", "hello", "world"], shell=True, capture_output=True, text=True)
r.stdout
'\n'
r.returncode
0
r.stderr
''
Measured: "hello" and "world" vanish completely. No exception, no non-zero exit code, no stderr output — just a blank line, because bare echo with no arguments prints nothing but the newline.
2. Where the missing arguments actually went
r = subprocess.run(["echo $0 $1 $2", "A", "B", "C"], shell=True, capture_output=True, text=True)
r.stdout
'A B C\n'
Measured: the first list item becomes the shell command string, and every item after it becomes a positional parameter ($0, $1, $2, …) available inside that shell command — not extra arguments appended to the program being run. In the first repro, "hello" and "world" became $0 and $1 for the shell session, but the command string "echo" never references them, so they're silently discarded.
3. Why: shell=True only ever runs args[0]
Per the Python subprocess docs, this is explicitly documented: with shell=True and a sequence for args, "the first item specifies the command string, and any additional items will be treated as additional arguments to the shell itself" — equivalent to running /bin/sh -c args[0] args[1] .... Our test confirms that's exactly what happens in 3.13.5 on Linux: it's not a bug, it's the documented contract, but it looks identical to the normal, safe list-of-arguments call that works correctly with shell=False.
4. Contrast: the same list works fine without shell=True
r = subprocess.run(["echo", "hello", "world"], shell=False, capture_output=True, text=True)
r.stdout
'hello world\n'
Measured: dropping shell=True (or just omitting it, since it defaults to False) makes the identical list behave as most people expect — every item becomes an argument to the program. The silent failure only happens when shell=True and a list are combined.
5. What to actually do
Don't mix shell=True with a list of arguments. If you need shell=True (to use shell features like pipes, globs, or environment variable expansion), pass a single string containing the whole command. If you just want to run a program with arguments safely, use a list with shell=False (the default) — that avoids both this silent-drop footgun and shell-injection risk from unsanitized input.
Frequently asked questions
Does subprocess.run([...], shell=True) raise an error when extra list items are ignored?
No — we measured it returning exit code 0 with empty stderr. There's no error or warning; the arguments are just silently unavailable to the program being run.
Where do the extra list items go when you use shell=True with a list?
We measured them becoming positional parameters ($0, $1, $2, ...) inside the shell command string itself, not arguments appended to the program — equivalent to running /bin/sh -c args[0] args[1] args[2] ...
Does the same list of arguments work correctly with shell=False?
Yes — we measured subprocess.run(["echo", "hello", "world"], shell=False) printing "hello world" correctly. The silent-drop behavior only happens when shell=True is combined with a list.
Is this subprocess.run(shell=True) behavior documented?
Yes — the Python docs state directly that with shell=True and a sequence, the first item is the command string and the rest become arguments to the shell itself, not the program. We confirmed this is still accurate in Python 3.13.5 on Linux.
How do I safely run a command with arguments using subprocess?
We'd use a list of the program and its arguments with shell=False (the default), and only use shell=True with a single complete command string when shell features like pipes or globs are actually needed.
Written with AI assistance. Measured on 2026-10-03 with Python 3.13.5 on Linux: called subprocess.run() with shell=True and a list of arguments the way you'd call it with shell=False, comparing stdout, stderr, and returncode against the string form and against shell=False, to see exactly what happens to the extra list items. PlainHeaders is our extension; Python's subprocess module is built in.