all posts

Best Sandbox APIs for Python Coding Agents in 2026

Ajay Kumar··10 min read

Most sandbox roundups are written for architects. This one is written for the person with a `tools.py` open, a LangGraph node half-finished, and a model that keeps trying to `pip install` something at 2pm on a Tuesday. The isolation-boundary debate matters — I'll get to it — but it is not the thing you'll spend your week on. You'll spend your week on whether the SDK is sync or async, whether `exec` streams, whether the file you wrote in tool call three still exists in tool call nine, and what happens to a runaway process when your framework's timeout fires. Those are Python-SDK ergonomics questions, and they decide whether the integration is an afternoon or a fortnight.

So: a Python-first pass. What the `pip install`-to-first-exec experience looks like, how each option handles streaming and cancellation, whether you can keep a stateful kernel across tool calls, and how it drops into an agent framework's tool schema. Then criteria, then the field, then a decision tree. The broader, framework-agnostic version of this argument lives at /blog/best-sandbox-apis-for-llm-agents-2026 — this is the narrower Python cut.

Disclosure: I built PandaStack, which is one of the entries below, so read this as a vendor's roundup and discount accordingly. The rule I hold to: specific numbers (latency, fork times) only for my own system, where I can point at the code that produces them. Every other platform is described qualitatively from its own public docs — no competitor latency figures, no prices, no free-tier limits. And SDK surfaces move faster than pricing does: method names, sync/async split, streaming semantics, and default timeouts all change between minor versions. Verify anything load-bearing against each vendor's current Python docs before you commit.

Why Python ergonomics come first

Every sandbox on this list will run a Python string and hand you stdout. That's the floor, and it's uninformative. The differences that bite show up in the second week, when your agent is real: a long `pytest` run that produces no output for four minutes because there's no streaming; a `write file, then run it` pattern that turns into base64-through-shell because the SDK has no filesystem primitive; an async framework calling a blocking SDK and pinning the event loop; a tool timeout that returns control to your loop while the sandbox keeps burning CPU on an infinite loop the model wrote. None of that is visible in a feature matrix. All of it is visible on day nine.

pip install to first exec

The honest benchmark for developer experience is: from `pip install <thing>`, how many lines and how many concepts until you have stdout back from a command that ran somewhere safe? The good answer is roughly three lines — import, create, exec — with an API key from an environment variable and a sensible default template. The warning sign is a setup that requires you to first define an image, then a deployment, then an app object, before anything executes. That ceremony may be justified by the platform's broader model, but it's a real cost for someone whose entire requirement is "run this string somewhere the model can't hurt me."

Sync vs async, and why it's not a detail

Modern Python agent code is overwhelmingly async. LangGraph nodes, `AsyncAnthropic`, FastAPI handlers, and any parallel-tool-call fan-out all live inside an event loop. A sandbox SDK that offers only blocking calls doesn't stop you — you wrap it in `asyncio.to_thread` — but you pay for it in thread-pool sizing, in traceback quality, and in cancellation semantics that quietly stop working, because cancelling a task that's blocked in a thread does not cancel the HTTP request underneath it. Check three things: is there a native async client, does it share a connection pool sensibly across concurrent sandboxes, and does `asyncio.CancelledError` propagate into an actual server-side cancellation or just abandon the request.

Streaming stdout and killing runaway processes

Two capabilities that read as nice-to-have and turn out to be structural. Streaming exec matters because agent tasks are long — a dependency install, a test suite, a build — and a blocking call that returns nothing for five minutes gives you no way to show progress, no way to detect a hung process early, and no way to truncate intelligently. Cancellation matters because model-generated code contains infinite loops at a rate that will genuinely surprise you. A client-side timeout that raises in your Python process but leaves the guest spinning is not a timeout; it's a leak with good manners. Ask specifically: does the SDK expose a server-enforced timeout, and is there a kill that takes effect inside the guest? PandaStack's answer is a TTL on the sandbox plus a per-exec timeout, so the worst case self-reaps; there's more on this pattern in /blog/agent-tool-timeouts-and-cancellation.

File transfer and whether state survives the turn

Coding agents are file-shaped. They write a module, run it, read a traceback, patch the module, run it again — and somewhere in there a CSV or a plot PNG needs to come back out. First-class `filesystem.write` / `filesystem.read` beats shelling out to `cat` with heredocs, which breaks the first time the model emits a quote character it shouldn't have. Then the bigger question: statefulness. Two distinct models exist and they are not interchangeable. A **persistent machine** keeps the filesystem across tool calls — turn three's `pip install` is still there at turn twelve. A **persistent kernel** additionally keeps the Python process alive, so variables defined in one cell exist in the next, Jupyter-style. Data-analysis agents want the second; most coding agents are fine with the first plus explicit file passing, which is more debuggable anyway. /blog/stateful-vs-ephemeral-ai-agent-sandboxes goes deeper on the trade.

How cleanly it becomes a tool

Whatever framework you're in — LangChain's `@tool`, LangGraph nodes, CrewAI tools, the OpenAI Agents SDK, or a hand-rolled Anthropic tool-use loop — the sandbox eventually has to become a JSON schema with a docstring, a handler, and a string result. The friction points are consistent: does the SDK return a clean result object you can serialize, or a stream you have to reassemble; is there a session handle you can stash in framework state between calls, or must you thread a raw ID through everything; and can you produce a deterministic, truncated string for the model without writing 40 lines of glue. The best answer is a small object with `stdout`, `stderr`, and `exit_code`, which is about three lines from being a tool result.

The criteria, beyond the SDK

Ergonomics decide your week; these decide your year. Run every candidate through the same six:

  1. Isolation boundary — does your code get its own guest kernel (hardware-virtualized microVM), a user-space kernel in front of the host (gVisor-style), or namespaces on the shared host kernel (a container)? This is the one you cannot retrofit, so ask the vendor directly rather than inferring from the word 'sandbox', which is not a regulated term.
  2. Cold start — an agent loop is a latency amplifier: twenty to forty sandbox calls per task means any per-call stall multiplies by trajectory length. Watch which number is quoted, too — warm pool, snapshot resume, and true cold boot are an order of magnitude apart and all get printed as 'startup time'.
  3. Statefulness — does a session survive across tool calls, for how long, and what happens when it idles because the user went to lunch? Filesystem persistence and kernel persistence are different features; check which one you're getting.
  4. Egress control — a perfectly isolated microVM with unrestricted internet can still exfiltrate whatever you put in it. Look for per-sandbox network policy, not an account-level firewall. See /blog/why-ai-agents-need-a-sandbox for why this half of the boundary gets forgotten.
  5. Pricing shape — I'm printing no prices for anyone, including myself, but the shape outlives the rate. The dominant question for agents: what do you pay while the sandbox idles waiting on a model call? Trajectories are mostly waiting, so duty cycle matters more than the headline rate.
  6. Self-hostability — sometimes a hard requirement (residency, air-gapped customer, compliance), sometimes an aesthetic preference that costs you an engineer. Be honest about which, and note that 'self-hosted' covers both open-source-you-run-end-to-end and bring-your-own-cloud with a proprietary control plane.

What the Python actually looks like

Concretely, here's the shape the criteria are arguing for, in PandaStack's Python SDK. Create per task, write the model's file in, run it with a timeout, branch on the exit code, and clean up in a `finally` so a crash in your orchestration doesn't leave a machine running until it turns up on an invoice:

from pandastack import Sandbox

# Reads PANDASTACK_API_KEY from the environment; base URL is configurable,
# so the same code targets hosted or your own self-hosted control plane.
# ttl_seconds is the safety net: if this process dies mid-run, the sandbox
# reaps itself instead of billing quietly until someone notices.
sbx = Sandbox.create(template="code-interpreter", ttl_seconds=900)

try:
    # First-class filesystem beats shelling out to `cat` with a heredoc,
    # which breaks the first time the model emits an unescaped quote.
    sbx.filesystem.write("/work/solve.py", model_written_code)

    # Server-enforced timeout, not just a client-side raise. A timeout that
    # returns control to your loop while the guest keeps spinning is a leak.
    r = sbx.exec("cd /work && python solve.py", timeout_seconds=60)

    # Branch on exit_code, never on "is stderr empty". Plenty of well-behaved
    # tools write to stderr and exit 0; plenty of broken ones exit 1 silently.
    if r.exit_code != 0:
        # stderr is the highest-value thing a sandbox gives an agent: it's the
        # correction signal. Truncate before it reaches your context window --
        # a script that prints 100MB will otherwise eat your token budget.
        feedback = r.stderr[-4000:]

    # Read artifacts back out rather than parsing them out of stdout.
    chart = sbx.filesystem.read("/work/out.png")
finally:
    sbx.kill()

On the numbers behind that, and only for my own system: create is snapshot-restore on every call with no warm pool — the restore step lands around 49ms, end-to-end create is 179ms p50 and roughly 203ms p99, and only the first-ever spawn of a brand-new template cold-boots at about 3 seconds to bake the snapshot. Forking a warm sandbox is 400–750ms same-host and 1.2–3.5s cross-host, which is what makes best-of-N repair affordable; managed Postgres on the same substrate takes 30–90s to create, because Postgres bootstrap is Postgres bootstrap. Networking is per-sandbox out of 16,384 pre-allocated /30 subnets per agent, which is where egress policy hangs.

Wiring it into an agent framework

The tool wrapper is where the SDK's shape either helps or fights you. Here it is twice — once as a LangChain/LangGraph `@tool`, once as a raw Anthropic tool-use handler — because between them they cover most of what people are actually building:

# --- 1. LangChain / LangGraph tool -----------------------------------------
from langchain_core.tools import tool
from pandastack import Sandbox


@tool
def run_python(code: str) -> str:
    """Execute Python code in an isolated sandbox and return its output.
    Use this to test code, inspect data, or verify a fix. The sandbox has
    no access to the host and is destroyed after the call."""
    sbx = Sandbox.create(template="code-interpreter", ttl_seconds=300)
    try:
        sbx.filesystem.write("/work/cell.py", code)
        r = sbx.exec("cd /work && python cell.py", timeout_seconds=45)
    finally:
        sbx.kill()

    # Return one deterministic, truncated string. The model reads this, so
    # exit status must be explicit -- don't make it infer failure from prose.
    return (
        f"exit_code={r.exit_code}\n"
        f"--- stdout ---\n{r.stdout[-6000:]}\n"
        f"--- stderr ---\n{r.stderr[-3000:]}"
    )


# --- 2. Plain Anthropic tool-use loop --------------------------------------
import anthropic

TOOLS = [
    {
        "name": "run_python",
        "description": (
            "Execute Python in an isolated microVM sandbox. Returns stdout, "
            "stderr, and the exit code. Nothing persists between calls."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "code": {"type": "string", "description": "Python source to run."}
            },
            "required": ["code"],
        },
    }
]

client = anthropic.Anthropic()


def handle_tool_use(block) -> dict:
    """Map one tool_use block to a tool_result block."""
    if block.name != "run_python":
        raise ValueError(f"unknown tool: {block.name}")
    output = run_python.invoke({"code": block.input["code"]})
    return {
        "type": "tool_result",
        "tool_use_id": block.id,
        "content": output,
        # Surface failure as is_error so the model treats it as a signal to
        # fix rather than as data to summarise.
        "is_error": output.startswith("exit_code=") and not output.startswith("exit_code=0"),
    }

Two notes that generalize past this SDK. First, one sandbox per tool call is the safe default but not the fast one — if your agent is iterating on a repo, hold a session across calls and stash the handle in framework state, or you'll re-run `pip install` on every turn. Second, keep the returned string boring and stable: models are much better at fixing code when the tool result has a predictable header than when it's a prose summary that varies run to run.

The anti-pattern: exec() with a blocklist

Before the roundup, the option people try first. It appears in an astonishing number of agent repos, usually with a comment claiming it's temporary:

# DO NOT SHIP THIS. It is on this page as a specimen, not a suggestion.

BANNED = ["import os", "import sys", "subprocess", "open(", "__import__", "eval"]


def run_model_code(code: str) -> str:
    """'Sandboxed' execution. It is not sandboxed."""
    for bad in BANNED:
        if bad in code:
            raise ValueError(f"blocked: {bad}")
    buf = io.StringIO()
    with contextlib.redirect_stdout(buf):
        exec(code, {"__builtins__": {}})  # noqa: S102
    return buf.getvalue()


# Things that sail straight through the blocklist above:
#
#   getattr(__builtins__, "__imp" + "ort__")("os").system("id")
#   [c for c in ().__class__.__base__.__subclasses__()
#      if c.__name__ == "BuiltinImporter"][0]
#   globals()[bytes([101,120,101,99]).decode()](payload)
#
# ...and roughly every other trick in twenty years of published CPython
# sandbox-escape write-ups -- all of which are in the model's training data,
# which is a genuinely funny place for your threat model to be.

The structural problem isn't that this particular blocklist is incomplete — it's that the approach can't be completed. CPython was never designed to contain hostile code in-process; the object graph is fully reachable from any expression, and `__subclasses__` walking has been the canonical escape for two decades. Stripping `__builtins__` removes the convenient doors, not the walls. And there's a specific irony to using pattern-matching defenses against a language model: you are trying to out-enumerate a system that has read every escape write-up ever published, including the ones about your exact blocklist idea. RestrictedPython and friends are real projects doing careful work, and they're honest about their scope — they're a policy layer for semi-trusted code, not a boundary against an adversary. Model output is adversarial by construction, not by intent. /blog/why-docker-is-not-a-sandbox makes the same argument one layer up the stack.

The field, through a Python lens

Qualitative for everyone but me, with the same caveat attached to each: these products move fast, so verify against their current docs before you build on a specific method name.

PandaStack (mine — read accordingly)

Open-source (Apache-2.0) Firecracker microVMs, self-hostable end to end on any Linux box with `/dev/kvm`. Python SDK is `pip install pandastack`, one `PANDASTACK_API_KEY`, and a three-line path to first exec. The API is deliberately small and orthogonal: create, exec, streaming exec, `filesystem` read/write/list, snapshot, fork, hibernate, kill — with a matching TypeScript SDK and CLI over the same REST surface, so the same code targets hosted or self-hosted via a base URL. Snapshot-restore on every create (no warm pool) makes per-turn creates cheap, and copy-on-write forking is first-class, which is the primitive best-of-N repair actually wants. Watch out for: vCPU and RAM are baked into the snapshot and can't be changed at restore, so per-run memory sizing means re-baking a template rather than passing a number — that's the price of the millisecond restore. Self-hosting is real operational weight; if you have no infra appetite, a hosted-only API is genuinely less work.

E2B

The most focused entry, and focus is a feature. E2B does sandboxes for AI agents and doesn't try to be a cloud platform, so the Python SDK surface is small and the docs are about the thing you're doing rather than the platform surrounding it. Firecracker-backed per its own infrastructure docs, hosted-first with an Apache-2.0 open-source core, with first-class Python and TypeScript SDKs and a code-interpreter heritage that shows in the ergonomics. Watch out for: anything else your product needs — a database, app hosting — is a separate vendor, and you should check the current self-host path against the repo rather than assuming. Verify the current exec/streaming/filesystem/persistence method names in its docs. See /blog/pandastack-vs-e2b.

Modal's Python DX is a genuine highlight and its centre of gravity is serverless AI/ML compute — GPU jobs, batch inference, training-adjacent work — with a Sandbox primitive alongside. If your agent already lives inside Modal's decorators-and-functions model, the sandbox drops in idiomatically rather than as a foreign object. Watch out for two things: the setup ceremony is higher than a bare sandbox SDK because you're adopting a programming model, not just a client; and Modal's own security docs describe gVisor as the isolation mechanism — a user-space kernel rather than a hardware-virtualized VM. That's a considered choice and a real step up from a plain container, but evaluate it as the different bet it is. Hosted-only. See /blog/pandastack-vs-modal.

Daytona

Daytona approaches this from the development-environment direction rather than the ephemeral-invocation one: sandboxes feel like machines you work in. Its docs describe a dedicated-kernel, complete-isolation model without naming a hypervisor, so I won't name one either. Open-source (AGPL-3.0) with managed, self-hosted, and hybrid deployment, and a Python SDK for creating and driving workspaces. Watch out for: environment semantics don't always map cleanly onto a thousand disposable creates per hour, and the license is worth reading against your distribution plans. Fits teams whose coding agents work inside longer-lived environments. See /blog/pandastack-vs-daytona.

Vercel Sandbox

Worth being direct: this one is TypeScript-first by design, tightly coupled to the Vercel AI SDK, and that coupling is the entire selling point. If your agent lives in a Next.js app, the integration tax is near zero. If you're a Python shop, you'd be reaching across an ecosystem boundary for a product whose value is being inside that ecosystem — usually the wrong trade. Vercel states plainly that sandboxes run as Firecracker microVMs; the client SDK is open source, the runtime is not, and there's no self-host path. See /blog/pandastack-vs-vercel-sandbox.

Fly.io Machines

A lower-level, more general primitive than the agent-shaped sandbox APIs: fast-starting VMs you drive through an API, deployable near users, with a persistence and scale-to-zero story that suits long-lived agent environments. The bet differs in kind — real machines that stick around and cost little when idle, rather than cheap disposable creates. For Python you're typically talking to the Machines HTTP API or a community client rather than a first-party agent SDK, which means you build the session lifecycle, the exec transport, cleanup, and the safety layer yourself. Fair trade for the flexibility; just budget for it honestly. See /blog/pandastack-vs-flyio-machines.

Runloop

Aimed squarely at the coding-agent case, with primitives shaped around what code agents actually do: durable dev boxes, repo-aware setup, and evaluation scaffolding for measuring whether your agent is improving. That last part matters more than it sounds — most teams eventually build a worse version of it internally, at the point where they can no longer tell whether last week's prompt change helped. Watch out for: a specialized platform is a bet on your use case staying that shape, and as with any newer entrant, verify the isolation model and Python SDK surface against current docs. See /blog/pandastack-vs-runloop.

DIY: Docker, gVisor, and restricted-Python

Three genuinely different things that get lumped together. **Docker** from Python (via the `docker` SDK) is the most common first move, and it's fine for code you wrote — but a container is namespaces and cgroups around a process on the host's one kernel, so the entire Linux syscall interface is your attack surface for code a model wrote. **gVisor** (runsc) is a real step up you can self-operate: a user-space kernel intercepts most syscalls before they reach the host, with workload-dependent compatibility and performance trade-offs, and it's the same software some managed platforms use. **Restricted-Python approaches** — RestrictedPython, AST allowlists, `exec` with stripped builtins — are a policy layer for semi-trusted code and should not be load-bearing against model output, for the reasons above. Rolling your own on Firecracker directly is also viable, and the VMM is the easy part: the work is per-tenant networking that doesn't leak addresses, snapshot storage, fleet scheduling, and reaping orphaned VMs before they quietly bankrupt you. I've built it; my estimate was wrong by a large multiple. Do it when scale justifies a team. More at /blog/build-vs-buy-firecracker-sandbox.

At a glance

  • PandaStack — Isolation: Firecracker microVM, own guest kernel per sandbox. Cold start: snapshot-restore on every create, 179ms p50 / ~203ms p99, ~3s only on a template's first-ever spawn. Stateful: filesystem persists for the session; hibernate/wake plus durable volumes for long runs. Self-host: yes, Apache-2.0, end to end on your own KVM hosts.
  • E2B — Isolation: Firecracker microVMs per its infrastructure docs. Cold start: optimized as the category headline; measure it yourself. Stateful: session-shaped with explicit lifetimes — confirm current semantics. Self-host: Apache-2.0 core, hosted-first; check the current path in the repo.
  • Modal — Isolation: gVisor user-space kernel per its security docs. Cold start: a serverless platform's start cost, not a sandbox product's headline metric. Stateful: shaped by Modal's programming model rather than a session handle. Self-host: no, hosted-only.
  • Daytona — Isolation: dedicated-kernel, VM-like per its docs; hypervisor not named here. Cold start: environment-oriented rather than per-invocation. Stateful: yes, that's the design centre. Self-host: yes, AGPL-3.0, plus managed and hybrid.
  • Vercel Sandbox — Isolation: Firecracker microVMs, stated plainly by Vercel. Cold start: verify in current docs. Stateful: bounded session semantics — check limits. Self-host: no; client SDK open source, runtime not. Also TypeScript-first.
  • Fly.io Machines — Isolation: hardware-virtualized VMs. Cold start: fast machine start, plus whatever session layer you build. Stateful: strong — persistence and scale-to-zero are the point. Self-host: no, but it's a general primitive you shape yourself.
  • Runloop — Isolation: verify against current docs. Cold start: dev-box oriented; verify. Stateful: durable dev boxes are a core primitive. Self-host: verify; it's a hosted product.
  • DIY Docker — Isolation: shared host kernel, namespaces and cgroups only — not a boundary for model output. Cold start: fast. Stateful: whatever you build. Self-host: entirely yours, including the on-call.
  • DIY gVisor — Isolation: user-space kernel in front of the host, real improvement over a container. Cold start: fast. Stateful: whatever you build. Self-host: yes, and you operate it.
  • restricted-Python — Isolation: none against an adversary; same process, same interpreter, same object graph. Cold start: instant. Stateful: in-process. Self-host: n/a. Use as a policy layer for semi-trusted code, never as a boundary.
Everything above is qualitative on purpose, and everything is dated the moment it's published. SDK method names get renamed, sync/async support arrives, isolation backends occasionally get swapped, licenses shift, and pricing changes on a timescale shorter than this post's shelf life. Use this to build a shortlist and understand the criteria, then pull every specific claim live from each vendor's own current documentation and note the date you read it. Then spend one afternoon on a real spike against your top two — your template, your region, your framework, your actual agent code — because an hour of measurement settles more than a week of reading roundups, including this one.

The decision tree

  • Pick E2B if you want a mature, focused, well-documented Python sandbox SDK with zero infrastructure to operate — the right default for most teams shipping their first coding agent.
  • Pick PandaStack if you want microVM isolation with cheap per-turn create and first-class copy-on-write forking for best-of-N repair, and you want the option to own the substrate — accepting that vCPU/RAM are fixed at bake time and self-hosting is real work.
  • Pick Modal if your agent already lives in Modal's Python programming model and the real workload is GPU or batch compute, and gVisor's boundary satisfies your threat model after you've actually read about it.
  • Pick Daytona if your coding agent works inside longer-lived, dev-environment-shaped workspaces rather than firing a thousand disposable creates per hour.
  • Pick Vercel Sandbox if you're already on the Vercel AI SDK — and note it's TypeScript-first, so a Python-only team is usually reaching across the wrong boundary.
  • Pick Fly.io Machines if persistence is the hard requirement and you're comfortable building the session lifecycle, exec transport, and safety layer yourself.
  • Pick Runloop if you're building a coding agent specifically and want dev-box semantics plus evaluation tooling rather than assembling that yourself later.
  • Pick DIY gVisor or raw Firecracker if the substrate is strategic at your scale and you can staff a team for it — and reread that sentence honestly before committing.
  • Pick a plain subprocess if the code is first-party code you wrote and reviewed. Wrapping a trusted script in a microVM buys latency and an on-call surface in exchange for protection against a threat that isn't in your model.
  • Pick nothing from a blocklist family — RestrictedPython, AST allowlists, `exec` with stripped builtins — as your only boundary for model output. It's a policy layer, not a wall.

The bottom line

There's no best sandbox API for Python coding agents — there's a best fit for the shape of your loop and the honesty of your threat model. The serious options broadly agree on the thing that matters most: code a model wrote needs a real boundary, and a shared kernel isn't one. Where they diverge is the part you'll live in daily — how many lines from `pip install` to first exec, whether there's a native async client, whether exec streams, whether a timeout actually stops the guest, whether files are first-class, whether state survives the turn, and whether you can own the substrate.

So work backwards from the capability your agent leans on hardest. If it's per-turn create cost, measure create latency on your own template in your own region. If it's branching — try five fixes, keep the one that passes — check fork semantics explicitly, because a snapshot you restore later is a backup and a fork of a live machine is a branch, and plenty of platforms have one without the other. Shortlist two, wire both behind the same `@tool` signature so swapping is a one-line change, and let a week of real agent traffic decide. PandaStack's bet, for the record, is an Apache-2.0 Firecracker core with a small, boring-in-the-good-way Python API — 179ms p50 create, 400–750ms same-host forks, streaming exec, first-class filesystem — that you can run end to end on your own hardware. If that matches your loop, integrate it against the field and keep us honest. If it doesn't, one of the others above genuinely fits you better, and I'd rather you use that than churn off mine in six months.

Frequently asked questions

What's the fastest way to add a sandbox to a Python coding agent?

Wrap it as a single tool with a stable, boring output contract, and don't over-engineer the first version. Concretely: `pip install` the SDK, put the API key in an environment variable, and write one function that creates a sandbox, writes the model's code to a file, execs it with an explicit timeout, and returns a truncated string containing the exit code, stdout, and stderr — then kill the sandbox in a `finally` block. That function becomes your LangChain `@tool`, your LangGraph node, or your Anthropic tool-use handler with almost no additional glue. Two details earn their keep immediately: always pass a timeout, because model-generated code contains infinite loops at a rate that will surprise you; and truncate output before it goes back into a prompt, because a script that accidentally prints a hundred megabytes will burn your context window and token budget in one call. Optimize later by holding a session across turns so you're not reinstalling dependencies every step.

Is `exec()` with a blocklist ever a safe way to run model-generated Python?

No, and the reason is structural rather than a matter of blocklist completeness. CPython was never designed to contain hostile code in-process: the full object graph is reachable from any expression, so `__subclasses__` walking, `getattr` on assembled strings, and attribute traversal from ordinary objects all reach the import machinery regardless of what you stripped from `__builtins__`. Every one of those techniques is documented in twenty years of published escape write-ups, which means they're in the training data of the model whose output you're trying to constrain — you are attempting to out-enumerate a system that has read all your prior art. RestrictedPython and similar projects do careful work and are honest about their scope: they're a policy layer for semi-trusted code, useful for limiting what a known-cooperative script may do, not a security boundary against an adversary. Model output is adversarial by construction, not by intent. Use a real boundary — a microVM with its own guest kernel, or at minimum a user-space kernel like gVisor — and treat any in-process restriction as defense in depth, never as the wall.

Do I need a stateful sandbox, or is one fresh sandbox per tool call fine?

It depends on which kind of statefulness you mean, because two different features get called the same thing. Filesystem persistence means the machine survives across tool calls, so a dependency installed on turn three is still there on turn twelve — that's what most coding agents want, since re-running `pip install` on every turn is the single most common source of wasted latency and money in a naive integration. Kernel persistence additionally keeps the Python process alive between calls, so variables defined in one cell exist in the next, Jupyter-style — that's what data-analysis agents want, and it's a genuinely different capability worth asking about explicitly. Fresh-per-call is the safest default and the right choice for stateless one-shot tools like 'evaluate this expression', because there's no state bleed between steps. For an iterative coding agent, hold a session across calls and stash the handle in your framework's state, but always set a TTL so a crashed loop reaps itself rather than leaving machines running.

How should timeouts and cancellation work from Python?

You want three layers, and most naive integrations only have one. First, a per-exec timeout that the server enforces inside the guest — a client-side timeout that raises in your Python process while the sandbox keeps burning CPU on an infinite loop is not a timeout, it's a leak with good manners. Second, a TTL on the sandbox itself, so that if your orchestrator crashes or a worker gets killed mid-run, the sandbox reaps itself instead of billing quietly until someone spots it on an invoice. Third, an explicit kill in a `finally` block, so the normal path cleans up promptly rather than waiting for the TTL. If you're on async Python, check specifically whether `asyncio.CancelledError` propagates into a real server-side cancellation or merely abandons the HTTP request — cancelling a task that's blocked inside `asyncio.to_thread` does not cancel the work underneath it, which is a common and expensive surprise. On PandaStack that maps to a `timeout_seconds` on exec, `ttl_seconds` on create, and `kill()` in the cleanup path.

Which isolation model should a Python coding agent require?

For arbitrary model-generated code, a hardware-virtualized microVM (Firecracker, Kata, Cloud Hypervisor) is the right default, because each sandbox gets its own guest kernel and the guest code never touches the host kernel — your exposed attack surface becomes a small, heavily-audited VMM rather than the entire Linux syscall interface. gVisor is a meaningful middle ground: a user-space kernel intercepts most syscalls before they reach the host, a real structural improvement over a plain container, with compatibility and performance trade-offs that vary by workload. Plain containers, however hardened with seccomp and namespaces, share the one host kernel, which is a large and continuously-evolving surface to expose to code nobody reviewed. Note that 'sandbox' isn't a regulated term, so ask any vendor directly whether your code gets its own kernel rather than inferring it from marketing copy. And don't read 'microVM' as 'unbreakable' — VMMs and KVM have both had bugs — so layer defenses regardless: a privilege-dropping jailer, seccomp on the VMM process itself, and per-sandbox network egress control.

Run code in a microVM in one API call.

49ms p50 cold start. Fork, snapshot, and scale to zero.

Start free
Written by Ajay Kumar, Founder, PandaStack.