Santiago Gutierrez
05/12/2026, 3:18 AMShellOperation().run() . The docs say that the run method takes kwargs to pass to Python's subprocess.Open . How can I achieve the following subprocess pattern using ShellOperation().run() :
import subprocess
proc = subprocess.Popen("my bash cmds here",
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
shell=True)
(stdoutStr, stderrStr) = proc.communicate()
if len(stderrStr) > 0:
# deal with errors in this sectionMarvin
05/12/2026, 3:18 AMMarvin
05/12/2026, 3:20 AMPopen(...).communicate() equivalent with ShellOperation().run().
What ShellOperation().run() does
- executes your commands in a shell script
- captures stdout and returns it as a list[str]
- captures stderr internally for logging/streaming
- raises RuntimeError if the command exits non-zero
So unlike subprocess.Popen(...).communicate(), it does not return (stdout, stderr) separately.
A basic pattern looks like this:
python
from prefect_shell import ShellOperation
try:
stdout_lines = ShellOperation(
commands=["my bash cmds here"],
stream_output=True, # optional: sends stdout/stderr to logs as they arrive
).run()
stdout_str = "\n".join(stdout_lines)
except RuntimeError as exc:
# non-zero exit code
# stderr is not returned separately here
print(f"Command failed: {exc}")
raise
If you specifically need stderr separately like your subprocess example, the easiest workaround is to redirect stderr yourself in the shell command:
python
from prefect_shell import ShellOperation
import tempfile
import os
stderr_file = tempfile.NamedTemporaryFile(delete=False)
stderr_path = stderr_file.name
stderr_file.close()
try:
stdout_lines = ShellOperation(
commands=[f"""
my bash cmds here 2> "{stderr_path}"
"""]
).run()
stdout_str = "\n".join(stdout_lines)
with open(stderr_path, "r") as f:
stderr_str = f.read()
if len(stderr_str) > 0:
# deal with errors here
print("stderr:", stderr_str)
finally:
os.unlink(stderr_path)
A few important notes:
- You do not need to pass `shell=True`; ShellOperation already runs your commands through a shell.
- run(**kwargs) does accept kwargs forwarded to subprocess.Popen, but that does not change the fact that run() only returns stdout.
- If your main goal is just “fail if there’s an error”, catching RuntimeError is the normal Prefect pattern.
So the recommendation is:
If you want Prefect-style execution
python
try:
stdout = ShellOperation(commands=["my bash cmds here"]).run()
except RuntimeError:
# handle command failure
...
If you need separate stderr content
- redirect 2> to a file
- then read that file after run()
Relevant docs/source:
- ShellOperation docs
- prefect-shell source
If you want, I can also show you a helper function that wraps ShellOperation().run() and returns (stdout_str, stderr_str) in a subprocess-like way.Santiago Gutierrez
05/12/2026, 3:22 AMIf you want, I can also show you a helper function that wraps ShellOperation().run() and returns (stdout_str, stderr_str) in a subprocess-like way.
Sure, show me.Marvin
05/12/2026, 3:22 AMMarvin
05/12/2026, 3:23 AMsubprocess-like interface on top of ShellOperation().run().
python
from prefect_shell import ShellOperation
import tempfile
import os
def run_shell_with_stderr(commands, stream_output=False, **open_kwargs):
"""
Run shell commands with Prefect ShellOperation and return
(stdout_str, stderr_str).
Args:
commands: str or list[str]
stream_output: whether to stream output to Prefect logs
**open_kwargs: kwargs forwarded to ShellOperation.run()
Returns:
tuple[str, str]: (stdout_str, stderr_str)
Raises:
RuntimeError: if the shell command exits non-zero
"""
if isinstance(commands, str):
commands = [commands]
stderr_file = tempfile.NamedTemporaryFile(delete=False)
stderr_path = stderr_file.name
stderr_file.close()
try:
redirected_commands = [
f"{{ {cmd}; }} 2>> \"{stderr_path}\"" for cmd in commands
]
stdout_lines = ShellOperation(
commands=redirected_commands,
stream_output=stream_output,
).run(**open_kwargs)
stdout_str = "\n".join(stdout_lines)
with open(stderr_path, "r") as f:
stderr_str = f.read()
return stdout_str, stderr_str
finally:
if os.path.exists(stderr_path):
os.unlink(stderr_path)
Usage:
python
stdout_str, stderr_str = run_shell_with_stderr(
"echo hello && echo warning >&2"
)
print("STDOUT:")
print(stdout_str)
print("STDERR:")
print(stderr_str)
And with error handling:
python
try:
stdout_str, stderr_str = run_shell_with_stderr(
"echo hello && ls /does-not-exist"
)
except RuntimeError as exc:
print(f"Command failed: {exc}")
One subtle thing here: if ShellOperation().run() raises RuntimeError, you may still want access to stderr. In that case, use this version instead:
python
from prefect_shell import ShellOperation
import tempfile
import os
def run_shell_with_stderr(commands, stream_output=False, **open_kwargs):
if isinstance(commands, str):
commands = [commands]
stderr_file = tempfile.NamedTemporaryFile(delete=False)
stderr_path = stderr_file.name
stderr_file.close()
stdout_str = ""
stderr_str = ""
try:
redirected_commands = [
f"{{ {cmd}; }} 2>> \"{stderr_path}\"" for cmd in commands
]
try:
stdout_lines = ShellOperation(
commands=redirected_commands,
stream_output=stream_output,
).run(**open_kwargs)
stdout_str = "\n".join(stdout_lines)
except RuntimeError:
with open(stderr_path, "r") as f:
stderr_str = f.read()
raise
with open(stderr_path, "r") as f:
stderr_str = f.read()
return stdout_str, stderr_str
finally:
if os.path.exists(stderr_path):
os.unlink(stderr_path)
If you want to preserve the stderr on failure, you can wrap the exception:
```python
from prefect_shell import ShellOperation
import tempfile
import os
class ShellRunError(RuntimeError):
def __init__(self, message, stdout="", stderr=""):
super().__init__(message)
self.stdout = stdout
self.stderr = stderr
def run_shell_with_stderr(commands, stream_output=False, **open_kwargs):
if isinstance(commands, str):
commands = [commands]
stderr_file = tempfile.NamedTemporaryFile(delete=False)
stderr_path = stderr_file.name
stderr_file.close()
stdout_str = ""
try:
redirected_commands = [
f"{{ {cmd}; }} 2>> \"{stderr_path}\"" for cmd in commands
]
try:
stdout_lines = ShellOperation(
commands=redirected_commands,
stream_output=stream_output,
).run(**open_kwargs)
stdout_str = "\n".join(stdout_lines)Marvin
05/12/2026, 3:23 AMThen:
```python
try:
stdout_str, stderr_str = run_shell_with_stderr(
"echo hello && ls /does-not-exist"
)
except ShellRunError as exc:
print("stdout:", exc.stdout)
print("stderr:", exc.stderr)
If you want, I can also give you:
- a version for multiple commands
- a version that returns exit code + stdout + stderr
- or a @task-wrapped Prefect version