DEV Community

John Wick
John Wick

Posted on

Fixing "bot_args Must Be a List" — A Common Python Subprocess Argument Bug

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

  1. The Bug in Isolation
  2. Why It's Silent
  3. What Actually Happens Character-by-Character
  4. The Correct Form
  5. How StayPresent Catches This
  6. Why This Matters More Than It Looks
  7. Full Example
  8. FAQs
  9. Conclusion

The Bug in Isolation

import subprocess

subprocess.run(["python", "bot.py"] + "--verbose")
Enter fullscreen mode Exit fullscreen mode

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']
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

A single-element list, not a bare string. For multiple arguments:

args = ["--verbose", "--config", "prod.yaml"]
Enter fullscreen mode Exit fullscreen mode

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
)
Enter fullscreen mode Exit fullscreen mode
staypresent.run(
    "bot.py",
    bot_args="--verbose",   # raises a clear error immediately
)
Enter fullscreen mode Exit fullscreen mode

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"},
)
Enter fullscreen mode Exit fullscreen mode

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"]},
])
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

Top comments (1)

Collapse
 
buildbasekit profile image
buildbasekit

Nice catch. Bugs caused by valid code doing the wrong thing are always harder to track down than syntax errors 😅