Guide
One package, many places to run code. This page is the one to read before the rest: what Code Sandboxes actually gives you over calling a backend's own SDK, what each backend still decides for you, and how to choose between them.
It used to weigh Code Sandboxes against E2B and Modal as alternatives.
They are not alternatives any more: both are backends this package drives,
alongside Daytona, CoreWeave and
Cloudflare. variant="e2b" and variant="modal" run
code on E2B and on Modal, through their own SDKs, with their own accounts and
their own bills.
So the question this page answers is not which of them to use instead. It is what the package adds over calling one of them directly, and what each of them still decides for you once you are on it.
What Code Sandboxes Adds​
- One result, whichever backend answered.
run_codecomes back as anExecutionResulteverywhere, with the failure split three ways: the sandbox (execution_ok), the process (exit_code), and the code itself (code_error). A caller that handles those handles every backend. - One vocabulary for the rest.
sandbox.files,sandbox.commands,create_context(),set_timeout(), names and tags mean the same thing on each backend and are mapped onto whatever it actually offers — a filesystem API where there is one, a snippet that decodes base64 where there is not. - CRUD in one place.
code-sandboxes list,get,createanddeletework across the backends that can answer them; see the management guide. - A refusal instead of a surprise. Where a backend cannot do what was
asked, the call says so rather than quietly doing something else:
network_policy="allowlist"on E2B is refused because E2B has no host allowlist,liston Cloudflare raises because the bridge cannot enumerate sandboxes, and a value that cannot cross as JSON is refused rather than mangled. - Nothing is required to be a cloud.
eval,monty,dockerandjupyter-serverrun on the machine in front of you, through the same interface as the cloud backends — which is what makes a test suite that exercises the interface cheap to run.
The package is BSD-3 licensed and no backend is privileged in it. Moving between them is a change of one string.
The Same Program, On Any Backend​
from code_sandboxes import Sandbox
for variant in ("eval", "e2b", "modal", "daytona", "coreweave"):
with Sandbox.create(variant=variant) as sandbox:
print(variant, sandbox.run_code("40 + 2").text) # "42" on each
What the interface cannot do is give a backend a capability it does not have. That is the other half of the comparison, and it is the table below.
What Each Backend Brings​
| Backend | Runs code in | State between calls | Rich display data | GPU | Management verbs |
|---|---|---|---|---|---|
cloudflare | a container on the edge, through a bridge Worker | none — a process per snippet | no | not exposed here | create, get, delete |
coreweave | a container on CoreWeave | a session process, falling back to none | no | gpu= | create, list, get, delete |
datalayer | a Datalayer runtime | a kernel | yes | gpu= | create, list, get, update, delete |
daytona | a Daytona sandbox | the code interpreter, one namespace per context | no | gpu=, spot capacity | create, list, get, update, delete |
e2b | a Firecracker microVM | a Jupyter kernel per context | yes | not exposed here | create, list, get, delete |
modal | a Modal container | a session process, falling back to none | no | gpu= | create, list, get, update, delete |
"Not exposed here" is about this package, not about the vendor: it means the
variant has no gpu= to pass on, not that the platform has no hardware.
The local backends answer the same interface with none of the accounts:
docker runs a container on this machine,
jupyter-server a kernel of a Jupyter Server,
monty a restricted Python subset in this process, and
eval plain exec() — which isolates nothing and is for
development and tests.
What The Package Calls On Your Behalf​
The SDKs underneath are still there, and this is roughly what a variant does with them. The point of the table is not that one column is better: it is that the left column is the same sentence in every row, and the others are not.
| Operation | Code Sandboxes | E2B SDK | Modal SDK |
|---|---|---|---|
| Create a sandbox | Sandbox.create(variant=...) | Sandbox.create() | modal.Sandbox.create(app=app, image=image) |
| Run a snippet | sandbox.run_code(code) | sandbox.run_code(code) | sandbox.exec("python", "-c", code) |
| Stream output | on_stdout=callback | on_stdout=callback | for line in process.stdout |
| A value back from the code | result.text, result.results | execution.results | — read it off stdout yourself |
| Shell command | sandbox.commands.run(cmd) | sandbox.commands.run(cmd) | sandbox.exec(*args) |
| Write a file | sandbox.files.write(path, content) | sandbox.files.write(path, content) | sandbox.open(path, "w") |
| Read a file | sandbox.files.read(path) | sandbox.files.read(path) | sandbox.open(path) |
| Extend the life | sandbox.set_timeout(seconds) | sandbox.set_timeout(seconds) | — |
| Terminate | sandbox.terminate() | sandbox.kill() | sandbox.terminate() |
| List what is running | get_manager(variant).list() | Sandbox.list() | modal.Sandbox.list() |
| A GPU | Sandbox.create(variant="modal", gpu="T4") | — | modal.Sandbox.create(gpu="T4") |
Where a backend has nothing for a row, the variant either builds it —
Cloudflare and CoreWeave run each snippet through a small program that reports
the trailing expression and the traceback, because exec on its own reports
neither — or refuses it, and its page says which.
Coming from Daytona​
# Daytona, directly
from daytona import CreateSandboxFromSnapshotParams, Daytona
client = Daytona()
sandbox = client.create(CreateSandboxFromSnapshotParams(labels={"app": "mine"}))
reply = sandbox.code_interpreter.run_code("print('hello')")
print(reply.result)
sandbox.delete()
# Through Code Sandboxes
from code_sandboxes import Sandbox
with Sandbox.create(variant="daytona") as sandbox:
result = sandbox.run_code("print('hello')")
print(result.stdout)
Both go through Daytona's CODE INTERPRETER rather than process.code_run, so
the namespace survives between calls either way. Credentials are Daytona's own
— DAYTONA_API_KEY, or DAYTONA_JWT_TOKEN with DAYTONA_ORGANIZATION_ID —
and DAYTONA_API_URL and DAYTONA_TARGET still choose the API and the region.
Two things the variant adds beyond the shared interface. The value of a
trailing expression is reported: Daytona's interpreter answers with what the
code PRINTED and drops the value of a last expression, so 1 + 1 returns
nothing there and "2" here. And a GPU is asked for by argument rather than by
assembling an image — gpu="H100", or gpu="H100,H200" for the fallback
order Daytona walks when the first is unavailable, with spot=True for
preemptible capacity and preempted_at() to find out whether it was
reclaimed. See Daytona.
Coming from E2B​
The sandbox is the same sandbox; what changes is the interface in front of it, and that a second backend costs one string.
# E2B, directly
from e2b_code_interpreter import Sandbox
sandbox = Sandbox.create()
execution = sandbox.run_code("print('hello')")
sandbox.kill()
# Through Code Sandboxes
from code_sandboxes import Sandbox
with Sandbox.create(variant="e2b") as sandbox:
result = sandbox.run_code("print('hello')")
print(result.stdout)
E2B_API_KEY is read the same way in both, and E2B_DOMAIN still points at a
self-hosted cluster. What the variant adds is the shared result model, the
management verbs, and the refusal where E2B's own model has no answer — see
E2B.
Coming from Modal​
# Modal, directly
import modal
app = modal.App.lookup("my-app", create_if_missing=True)
sandbox = modal.Sandbox.create(app=app, gpu="T4")
process = sandbox.exec("python", "-c", "print('hello')")
print(process.stdout.read())
sandbox.terminate()
# Through Code Sandboxes
from code_sandboxes import Sandbox
with Sandbox.create(variant="modal", gpu="T4") as sandbox:
result = sandbox.run_code("print('hello')")
print(result.stdout)
Credentials are Modal's own — modal token new, or MODAL_TOKEN_ID with
MODAL_TOKEN_SECRET. See Modal.
Choosing a Backend​
| Use case | Backend |
|---|---|
| Development and tests, no account, no isolation | eval |
| LLM-generated snippets that must not touch the host | monty |
| Notebook-shaped work with a kernel, locally | jupyter-server, docker |
| An agent session that keeps its variables, in a cloud | e2b, daytona, datalayer |
| Figures and HTML back from the code | e2b, datalayer, jupyter-server |
| ML training on a GPU | coreweave, modal, daytona, datalayer |
| The cheapest GPU capacity, preemption accepted | daytona with spot=True |
| Short snippets close to the user, no state to keep | cloudflare |
| Production runtimes with snapshots and quotas | datalayer |
What This Package Is Not​
It is not a compute platform. Capacity, regions, quotas, cold starts and the
bill belong to the backend you chose, and no interface in front of them changes
any of it. It adds no isolation either: an eval sandbox is exec() in this
process however carefully the result is reported, and a sandbox is only as
contained as the backend running it.
What it does is make the choice reversible.