Skip to main content
This guide shows you how to run commands in sandboxes using the exec() method. Use it when you need to run shell commands, scripts, or interactive interpreters inside a sandbox and want to capture their output, stream it in real time, send input on stdin, or control how non-zero exit codes are handled. The guide is for developers who already work with the cwsandbox Python client.

Run a basic command

Start with a single command that runs to completion and returns its captured output. The exec() method returns a Process handle:
from cwsandbox import Sandbox

with Sandbox.run() as sandbox:
    # Run a command and get the result
    result = sandbox.exec(["echo", "Hello, World!"]).result()

    print(result.stdout)      # "Hello, World!\n"
    print(result.returncode)  # 0

Get results

exec() returns immediately so you can decide how to wait for the command. Call .result() on the Process handle to block for the output:
# Returns Process immediately
process = sandbox.exec(["python", "-c", "print('hello')"])

# Block for result
result = process.result()
print(result.stdout)      # "hello\n"
print(result.stderr)      # ""
print(result.returncode)  # 0

# One-liner pattern
result = sandbox.exec(["ls", "-la"]).result()

Stream output

Blocking on .result() waits for the command to finish before you see any output. To observe progress as it happens, iterate over process.stdout before calling .result():
# Returns Process immediately
process = sandbox.exec(["python", "long_script.py"])

# Stream stdout line by line
for line in process.stdout:
    print(f"[stdout] {line}", end="")

# Get final result
result = process.result()
print(f"Exit code: {result.returncode}")
Use streaming when you need to:
  • Monitor long-running processes.
  • Process output as it arrives.
  • Implement progress indicators.

Stream input to stdin

Some commands need input written to stdin while they run, such as pipelines, interactive interpreters, or tools that read until EOF. Send input to running commands by enabling stdin with stdin=True:
with Sandbox.run() as sandbox:
    process = sandbox.exec(["cat"], stdin=True)

    process.stdin.write(b"hello world\n").result()
    process.stdin.close().result()

    result = process.result()
    print(result.stdout)  # "hello world\n"

StreamWriter methods

When stdin=True, process.stdin is a StreamWriter with three methods:
  • write(data: bytes): Write raw bytes. Returns OperationRef[None].
  • writeline(text: str): Write text with a trailing newline (encodes to UTF-8). Returns OperationRef[None].
  • close(): Signal EOF. The system completes pending writes first. Returns OperationRef[None].
When stdin=False (the default), process.stdin is None.

Send multiple writes

Send data incrementally before closing:
process = sandbox.exec(["cat"], stdin=True)

process.stdin.writeline("line 1").result()
process.stdin.writeline("line 2").result()
process.stdin.writeline("line 3").result()
process.stdin.close().result()

result = process.result()
print(result.stdout)  # "line 1\nline 2\nline 3\n"

Run interactive Python through stdin

Feed Python code to an interactive interpreter:
process = sandbox.exec(["python3"], stdin=True)

process.stdin.writeline("x = 40 + 2").result()
process.stdin.writeline("print(f'answer: {x}')").result()
process.stdin.close().result()

result = process.result()
print(result.stdout)  # "answer: 42\n"

Combine stdin and stdout streaming

Stream output while sending input:
process = sandbox.exec(["cat"], stdin=True)

# Send input
process.stdin.writeline("hello").result()
process.stdin.writeline("world").result()
process.stdin.close().result()

# Stream output as it arrives
for line in process.stdout:
    print(f"[out] {line}", end="")

result = process.result()

Handle EOF-dependent commands

Some commands (like sort) read all input before producing output. Close stdin to signal EOF:
process = sandbox.exec(["sort"], stdin=True)

process.stdin.writeline("banana").result()
process.stdin.writeline("apple").result()
process.stdin.writeline("cherry").result()
process.stdin.close().result()  # sort requires EOF before producing output

result = process.result()
print(result.stdout)  # "apple\nbanana\ncherry\n"

Use stdin in async contexts

In async contexts, await each OperationRef directly:
async with Sandbox.run() as sandbox:
    process = sandbox.exec(["cat"], stdin=True)

    await process.stdin.write(b"async hello\n")
    await process.stdin.close()

    result = await process
    print(result.stdout)  # "async hello\n"

When to use stdin=True compared with stdin=False

ScenariostdinReason
Run a command with argumentsFalseInput comes from args, not stdin
Pipe data into a commandTrueThe command reads from stdin
Interactive interpreterTrueThe interpreter reads commands from stdin
Process that reads until EOFTrueRequires close() to signal EOF
Fire-and-forget commandFalseNo input needed

Set the working directory

By default, commands run from the sandbox’s default working directory. Override that with cwd:
result = sandbox.exec(
    ["ls", "-la"],
    cwd="/app/data",
).result()
The path must be absolute.

Set a timeout

To stop a command that might freeze or run longer than you expect, set a timeout with timeout_seconds:
from cwsandbox import SandboxTimeoutError

try:
    result = sandbox.exec(
        ["sleep", "60"],
        timeout_seconds=5.0,
    ).result()
except SandboxTimeoutError:
    print("Command timed out")

Handle errors with check

The check parameter controls error behavior for non-zero exit codes:

Default behavior with check=False

Returns the result regardless of exit code:
result = sandbox.exec(["false"]).result()
print(result.returncode)  # 1 (no exception)

Raise on failure with check=True

Raises SandboxExecutionError on non-zero exit:
from cwsandbox import SandboxExecutionError

try:
    result = sandbox.exec(
        ["python", "-c", "raise ValueError('oops')"],
        check=True,
    ).result()
except SandboxExecutionError as e:
    print(f"Command failed: {e.exec_result.returncode}")
    print(f"stderr: {e.exec_result.stderr}")

Run Python code

Sandboxes are commonly used to run Python code, either as short one-liners or as longer scripts passed inline:
# One-liner
result = sandbox.exec(
    ["python", "-c", "import sys; print(sys.version)"],
).result()

# Script from string
code = '''
import json
data = {"result": 42}
print(json.dumps(data))
'''
result = sandbox.exec(["python", "-c", code]).result()
output = json.loads(result.stdout)

Sequential compared with parallel execution

Choose sequential execution when later commands depend on earlier ones, and parallel execution when commands are independent and you want them to run concurrently across sandboxes.

Run commands sequentially when order matters

# Dependencies require sequential execution
sandbox.exec(["pip", "install", "requests"]).result()
sandbox.exec(["python", "script_using_requests.py"]).result()

Run independent commands in parallel

# Start multiple sandboxes
sandboxes = [Sandbox.run() for _ in range(3)]

# Start commands on each
processes = [
    sb.exec(["python", "-c", f"print({i})"])
    for i, sb in enumerate(sandboxes)
]

# Collect all results
results = [p.result() for p in processes]
for r in results:
    print(r.stdout)

Wait for N of M processes to complete

Use cwsandbox.wait() to wait for a subset of processes:
import cwsandbox

processes = [sb.exec(["python", "task.py"]) for sb in sandboxes]

# Wait for first 2 to complete
done, pending = cwsandbox.wait(processes, num_returns=2)

# Process completed ones immediately
for p in done:
    print(p.result().stdout)

# Wait for remaining
for p in pending:
    print(p.result().stdout)

Control processes

When a command keeps running after the exec() call returns, you can check on it without blocking or wait for it to finish on your own terms. The Process handle provides methods for monitoring and control:
process = sandbox.exec(["python", "server.py"])

# Check if running (non-blocking)
if process.poll() is None:
    print("Still running")

# Wait for completion
exit_code = process.wait()
print(f"Exited with code: {exit_code}")
Last modified on May 29, 2026