RustPython/RustPython · error · ValueError
cmd must be a string
Error message
cmd must be a string
What it means
loop.subprocess_shell() only accepts the command as a single str or bytes value, because it is executed via '/bin/sh -c'. Anything else (typically a list) raises ValueError('cmd must be a string') before any process is spawned. Argument lists belong to loop.subprocess_exec(), which takes the executable and argv without a shell — mirroring the stdlib subprocess shell=True convention.
Source
Thrown at Lib/asyncio/base_events.py:1754
if stdout is not None and stderr == subprocess.STDOUT:
info.append(f'stdout=stderr={_format_pipe(stdout)}')
else:
if stdout is not None:
info.append(f'stdout={_format_pipe(stdout)}')
if stderr is not None:
info.append(f'stderr={_format_pipe(stderr)}')
logger.debug(' '.join(info))
async def subprocess_shell(self, protocol_factory, cmd, *,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=False,
shell=True, bufsize=0,
encoding=None, errors=None, text=None,
**kwargs):
if not isinstance(cmd, (bytes, str)):
raise ValueError("cmd must be a string")
if universal_newlines:
raise ValueError("universal_newlines must be False")
if not shell:
raise ValueError("shell must be True")
if bufsize != 0:
raise ValueError("bufsize must be 0")
if text:
raise ValueError("text must be False")
if encoding is not None:
raise ValueError("encoding must be None")
if errors is not None:
raise ValueError("errors must be None")
protocol = protocol_factory()
debug_log = None
if self._debug:
# don't log parameters: they may contain sensitive information
# (password) and may be too longView on GitHub (pinned to aaeab4f754)
Solutions
- Switch to loop.subprocess_exec(protocol_factory, *cmd_list) for list-style commands
- Join into one shell string with shlex.join(cmd) and keep subprocess_shell (only if a shell is genuinely required)
- Validate the cmd type at the wrapper boundary and select shell vs exec mode accordingly
Example fix
# before proc = await loop.subprocess_shell(proto, ['ffmpeg', '-i', in_path]) # after proc = await loop.subprocess_exec(proto, 'ffmpeg', '-i', in_path)
Defensive patterns
Strategy: type-guard
Validate before calling
def spawn(loop, proto_factory, cmd):
if isinstance(cmd, (list, tuple)):
return loop.subprocess_exec(proto_factory, *cmd)
return loop.subprocess_shell(proto_factory, cmd) Type guard
def is_shell_command(cmd) -> bool:
return isinstance(cmd, (str, bytes)) Prevention
- Reserve list argv for subprocess_exec and single strings for subprocess_shell
- Prefer subprocess_exec for structured commands — it avoids shell quoting bugs entirely
- Validate the command type once at the API boundary of your process wrapper
When it happens
Trigger: loop.subprocess_shell(make_protocol, ['ls', '-l']); forwarding an argv list built for subprocess.run() into the shell API; passing a tuple or other non-str/bytes command object.
Common situations: Porting subprocess.run(cmd_list, shell=True) code to asyncio; generic process wrappers that must serve both shell and exec modes and default to shell; commands assembled at runtime from lists.
Related errors
- loop must be an instance of AbstractEventLoop or None, not '
- policy must be an instance of AbstractEventLoopPolicy or Non
- A future is required for source argument
- A future is required for destination argument
- {future!r} object does not appear to be compatible with asyn
AI-assisted analysis of RustPython/RustPython@aaeab4f754 (2026-08-17).
Data as JSON: /api/errors/8fbe6ec378625030.
Report an issue: GitHub.