Running commands in Sandboxes

Once you have created a Sandbox, you can run commands inside it using the Sandbox.exec method.

sb = modal.Sandbox.create(app=my_app)

process = sb.exec("echo", "hello")
print(process.stdout.read())

process = sb.exec("python", "-c", "print(1 + 1)")
print(process.stdout.read())

process = sb.exec(
    "bash",
    "-c",
    "for i in $(seq 1 10); do echo foo $i; sleep 0.1; done",
)
for line in process.stdout:
    print(line, end="")

sb.terminate()

Sandbox.exec returns a ContainerProcess object, which allows access to the process’s stdout, stderr, and stdin.

Input

The Sandbox and ContainerProcess stdin handles are StreamWriter objects. This object supports flushing writes with both synchronous and asynchronous APIs:

import asyncio

sb = modal.Sandbox.create(app=my_app)

p = sb.exec("bash", "-c", "while read line; do echo $line; done")
p.stdin.write(b"foo bar\n")
p.stdin.write_eof()
p.stdin.drain()
p.wait()
sb.terminate()

async def run_async():
    sb = await modal.Sandbox.create.aio(app=my_app)
    p = await sb.exec.aio("bash", "-c", "while read line; do echo $line; done")
    p.stdin.write(b"foo bar\n")
    p.stdin.write_eof()
    await p.stdin.drain.aio()
    await p.wait.aio()
    await sb.terminate.aio()

asyncio.run(run_async())

Output

The Sandbox and ContainerProcess stdout and stderr handles are StreamReader objects. These objects support reading from the stream in both synchronous and asynchronous manners.

To read from a stream after the underlying process has finished, you can use the read method, which blocks until the process finishes and returns the entire output stream.

sb = modal.Sandbox.create(app=my_app)
p = sb.exec("echo", "hello")
print(p.stdout.read())
sb.terminate()

To stream output, take advantage of the fact that stdout and stderr are iterable:

import asyncio

sb = modal.Sandbox.create(app=my_app)

p = sb.exec("bash", "-c", "for i in $(seq 1 10); do echo foo $i; sleep 0.1; done")

for line in p.stdout:
    # Lines preserve the trailing newline character, so use end="" to avoid double newlines.
    print(line, end="")
p.wait()
sb.terminate()

async def run_async():
    sb = await modal.Sandbox.create.aio(app=my_app)
    p = await sb.exec.aio("bash", "-c", "for i in $(seq 1 10); do echo foo $i; sleep 0.1; done")
    async for line in p.stdout:
        # Avoid double newlines by using end="".
        print(line, end="")
    await p.wait.aio()
    await sb.terminate.aio()

asyncio.run(run_async())

Stream types

By default, all streams are buffered in memory, waiting to be consumed by the client. You can control this behavior with the stdout and stderr parameters. These parameters are conceptually similar to the stdout and stderr parameters of the subprocess module.

from modal.stream_type import StreamType

sb = modal.Sandbox.create(app=my_app)

# Default behavior: buffered in memory.
p = sb.exec(
    "bash",
    "-c",
    "echo foo; echo bar >&2",
    stdout=StreamType.PIPE,
    stderr=StreamType.PIPE,
)
print(p.stdout.read())
print(p.stderr.read())

# Print the stream to STDOUT as it comes in.
p = sb.exec(
    "bash",
    "-c",
    "echo foo; echo bar >&2",
    stdout=StreamType.STDOUT,
    stderr=StreamType.STDOUT,
)
p.wait()

# Discard all output.
p = sb.exec(
    "bash",
    "-c",
    "echo foo; echo bar >&2",
    stdout=StreamType.DEVNULL,
    stderr=StreamType.DEVNULL,
)
p.wait()

sb.terminate()