Build a stateful, sandboxed code interpreter

This example demonstrates how to build a stateful code interpreter using a Modal Sandbox.

We’ll create a Modal Sandbox that listens for code to execute and then executes the code in a Python interpreter. Because we’re running in a sandboxed environment, we can safely use the “unsafe” exec() to execute the code.

Setting up a code interpreter in a Modal Sandbox

Our code interpreter uses a Python “driver program” to listen for code sent in JSON format to its standard input (stdin), execute the code, and then return the results in JSON format on standard output (stdout).

import inspect
import json
from typing import Any

import modal


def driver_program():
    import json
    import sys
    from contextlib import redirect_stderr, redirect_stdout
    from io import StringIO

    # When you `exec` code in Python, you can pass in a dictionary
    # that defines the global variables the code has access to.

    # We'll use that to store state.

    globals: dict[str, Any] = {}
    while True:
        command = json.loads(input())  # read a line of JSON from stdin
        if (code := command.get("code")) is None:
            print(json.dumps({"error": "No code to execute"}))
            continue

        # Capture the executed code's outputs
        stdout_io, stderr_io = StringIO(), StringIO()
        with redirect_stdout(stdout_io), redirect_stderr(stderr_io):
            try:
                exec(code, globals)
            except Exception as e:
                print(f"Execution Error: {e}", file=sys.stderr)

        print(
            json.dumps(
                {
                    "stdout": stdout_io.getvalue(),
                    "stderr": stderr_io.getvalue(),
                }
            ),
            flush=True,
        )

Now that we have the driver program, we can write a function to take a ContainerProcess that is running the driver program and execute code in it.

def run_code(p: modal.container_process.ContainerProcess, code: str):
    p.stdin.write(json.dumps({"code": code}))
    p.stdin.write("\n")
    p.stdin.drain()
    next_line = next(iter(p.stdout))
    result = json.loads(next_line)
    print(result["stdout"], end="")
    print("\033[91m" + result["stderr"] + "\033[0m", end="")

We’ve got our driver program and our code runner. Now we can create a Sandbox and run the driver program in it.

We have to convert the driver program to a string to pass it to the Sandbox. Here we use inspect.getsource to get the source code as a string, but you could also keep the driver program in a separate file and read it in.

driver_program_text = inspect.getsource(driver_program)
driver_program_command = f"""{driver_program_text}\n\ndriver_program()"""

app = modal.App.lookup("code-interpreter", create_if_missing=True)
sb = modal.Sandbox.create(app=app)
p = sb.exec("python", "-c", driver_program_command)

Running code in a Modal Sandbox

Now we can execute some code in the Sandbox!

run_code(p, "print('hello, world!')")  # hello, world!

The Sandbox and our code interpreter are stateful, so we can define variables and use them in subsequent code.

run_code(p, "x = 10")
run_code(p, "y = 5")
run_code(p, "result = x + y")
run_code(p, "print(f'The result is: {result}')")  # The result is: 15

We can also see errors when code fails.

run_code(p, "print('Attempting to divide by zero...')")
run_code(p, "1 / 0")  # Execution Error: division by zero

Finally, let’s clean up after ourselves and terminate the Sandbox.

sb.terminate()