Why passing a string instead of a list of CLI arguments silently breaks a python subprocess, and how StayPresent catches it with a clear error.
Fixing "bot_args Must Be a List" — A Common Python Subprocess Argument Bug
Here's a bug that's easy to write and genuinely confusing to debug: you want to pass a single command-line flag to a subprocess, so you write args="--verbose" instead of args=["--verbose"]. No error. No crash. Just... weird, silently wrong behavior. This post explains exactly why that happens, and how to avoid it — including how StayPresent catches this specific mistake before it ever reaches your subprocess.
Table of Contents
- The Bug in Isolation
- Why It's Silent
- What Actually Happens Character-by-Character
- The Correct Form
- How StayPresent Catches This
- Why This Matters More Than It Looks
- Full Example
- FAQs
- Conclusion
The Bug in Isolation
import subprocess
subprocess.run(["python", "bot.py"] + "--verbose")
This doesn't raise an error. It also doesn't do what you probably wanted.
Why It's Silent
In Python, a string is iterable — iterating over "--verbose" yields individual characters: '-', '-', 'v', 'e', 'r', 'b', 'o', 's', 'e'. When you concatenate a list with a string using +, Python doesn't error — string concatenation with + between a list and a str actually does raise TypeError: can only concatenate list (not "str") to list, so that specific example fails loudly. But the far more common and dangerous version of this bug happens when a function accepts an args parameter and does something like list(args) or spreads it with *args internally — silently exploding a string into single-character list elements instead of raising anything at all.
What Actually Happens Character-by-Character
args = "--verbose"
print(list(args))
# ['-', '-', 'v', 'e', 'r', 'b', 'o', 's', 'e']
If this ends up being passed as command-line arguments to a subprocess, your process receives nine separate single-character arguments instead of one --verbose flag — which your argument parser then either silently ignores, or fails on in a way that has nothing to do with the actual mistake, making it genuinely hard to trace back to "I passed a string instead of a list" as the root cause.
The Correct Form
args = ["--verbose"]
A single-element list, not a bare string. For multiple arguments:
args = ["--verbose", "--config", "prod.yaml"]
How StayPresent Catches This
StayPresent's bot_args parameter accepts exactly this kind of CLI argument list — and it explicitly validates that what you pass is actually a list, not a bare string:
staypresent.run(
"bot.py",
bot_args=["--verbose"], # correct
)
staypresent.run(
"bot.py",
bot_args="--verbose", # raises a clear error immediately
)
Passing a bare string raises a clear error right away, rather than silently exploding into individual characters and handing your bot process nine garbage arguments with no indication of why. This is a small but genuinely useful piece of input validation — the alternative (no validation at all) is exactly the silent, hard-to-trace bug described above.
Why This Matters More Than It Looks
This class of bug is common specifically because it's easy to write and doesn't announce itself. A developer testing locally with a single flag, writing args="--debug" instead of args=["--debug"], might never notice anything wrong if their argument parser happens to tolerate or ignore the garbage single-character arguments — until a teammate adds a second flag, the bug manifests differently, and now there are two confusing symptoms to debug instead of one obvious TypeError.
Full Example
import staypresent
staypresent.web.json({"status": "running"})
staypresent.run(
"bot.py",
bot_args=["--verbose", "--sync-commands"],
env={"BOT_ENV": "production"},
)
For multiple bots with different arguments, the same rule applies per entry in bots:
staypresent.run(bots=[
{"file": "telegram_bot.py", "args": ["--verbose"]},
{"file": "discord_bot.py", "args": ["--sync-commands"]},
])
FAQs
Does this bug only affect StayPresent?
No — it's a general Python subprocess pitfall (subprocess.Popen, subprocess.run, os.execvp, and similar all expect a list of argument strings). StayPresent's specific contribution is validating bot_args up front and raising a clear error instead of letting the mistake propagate silently.
What if I only have one argument — do I still need a list?
Yes — ["--verbose"], not "--verbose". A single-element list is still a list.
Is there a way to pass a raw command-line string and have it parsed automatically?
Not through bot_args directly — if you have a full command-line string from elsewhere, use shlex.split() to turn it into a proper list first: bot_args=shlex.split(raw_string).
Conclusion
The string vs list argument bug is one of Python's quieter traps — a string being iterable means a simple typo doesn't raise an error, it just silently does the wrong thing. StayPresent's explicit validation on bot_args turns this specific mistake into an immediate, clear error instead of a confusing subprocess failure you'd otherwise have to reverse-engineer.
pip install staypresent[prod]
Top comments (1)
Nice catch. Bugs caused by valid code doing the wrong thing are always harder to track down than syntax errors 😅