Run untrusted Python in a machine of its own
A real CPython on a real Linux box, with pip, a compiler and a network — isolated as a dedicated
virtual machine and destroyed when you are finished with it. No interpreter patching, no
__builtins__ tricks, no import allowlist to maintain.
In-process Python sandboxing does not work
Every few years someone rediscovers the idea of running untrusted Python inside a trusted Python by
stripping __builtins__, blocking import, and filtering attribute access.
It is an appealing idea because it is cheap, and it fails for a structural reason: the attack surface
is the entire CPython implementation, hundreds of thousands of lines of C, reachable through an
introspection system designed to expose everything.
The canonical demonstration is pysandbox, which its author eventually withdrew with the
blunt summary that it is broken by design — the advice being to run Python in a sandbox rather
than putting a sandbox in Python. RestrictedPython, which is still maintained and still useful, is
careful to describe itself as a way to define a restricted subset for a trusted environment,
not as a security boundary. Both projects are telling you the same thing: the boundary has to be below
the interpreter.
Below the interpreter, the options are process-level restriction (seccomp, namespaces), a container, or a virtual machine. Containers share the host kernel, so kernel bugs are a shared-fate problem across everything on that host. A virtual machine has its own kernel, which moves the boundary to the hypervisor. That is the trade this service makes: each sandbox is a dedicated VM, and the interpreter inside it is completely unmodified.
A data-analysis job, start to finish
Install what the job needs, run the script, take the answer out through stdout, destroy the machine.
import os, json, base64, requests
API = "https://sandbox-as-a-service.com/v1"
H = {"Authorization": f"Bearer {os.environ['AAS_API_KEY']}"}
sbx = requests.post(f"{API}/sandboxes", headers=H,
json={"size": "medium", "timeout_minutes": 20}).json()
sid = sbx["id"]
def run(cmd, **kw):
return requests.post(f"{API}/sandboxes/{sid}/exec", headers=H,
json={"command": cmd, **kw}).json()
try:
# 1. Dependencies. Give the install a generous per-command timeout.
run("python3 -m venv .venv && .venv/bin/pip install -q pandas matplotlib",
timeout_ms=420_000)
# 2. The script the model wrote. Heredoc keeps quoting sane.
script = '''
import pandas as pd, matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
df = pd.read_csv("data.csv")
print(df.describe().to_json())
df["value"].hist()
plt.savefig("hist.png", dpi=110)
'''
run(f"cat > analyse.py <<'PY'\n{script}\nPY")
# 3. Execute with an explicit, non-interactive environment.
r = run(".venv/bin/python analyse.py",
timeout_ms=180_000,
cwd="/home/sandbox",
env={"PYTHONUNBUFFERED": "1", "MPLBACKEND": "Agg"})
if r["exit_code"] != 0:
raise RuntimeError(r["stderr"])
stats = json.loads(r["stdout"])
# 4. Pull the image out as base64 — nothing on disk survives teardown.
img = run("base64 -w0 hist.png")["stdout"]
open("hist.png", "wb").write(base64.b64decode(img))
finally:
requests.delete(f"{API}/sandboxes/{sid}", headers=H)SBX=$(curl -sX POST https://sandbox-as-a-service.com/v1/sandboxes \
-H "Authorization: Bearer $AAS_API_KEY" -H "Content-Type: application/json" \
-d '{"size":"medium","timeout_minutes":20}' | jq -r .id)
exec_cmd() {
curl -sX POST https://sandbox-as-a-service.com/v1/sandboxes/$SBX/exec \
-H "Authorization: Bearer $AAS_API_KEY" -H "Content-Type: application/json" \
-d "$1"
}
exec_cmd '{"command":"pip install --user -q polars pyarrow","timeout_ms":300000}'
exec_cmd '{"command":"python3 -c \"import polars as pl; print(pl.__version__)\"","env":{"PYTHONUNBUFFERED":"1"}}'
curl -sX DELETE https://sandbox-as-a-service.com/v1/sandboxes/$SBX \
-H "Authorization: Bearer $AAS_API_KEY"
Practical notes
Quoting
The single largest source of bugs when a model generates the code is quoting. A Python snippet passed
through python3 -c "..." inside a JSON string inside a shell command has three layers of
escaping and will eventually go wrong on an apostrophe. Write the file with a quoted heredoc
(<<'PY', which suppresses shell expansion) and then execute the file. It also gives
you real line numbers in tracebacks, which matters when the traceback is what you feed back to the model.
Installs are the slow part
Creating a sandbox is fast relative to installing a scientific stack. If your workload always needs the
same packages, do the install once per sandbox and reuse the sandbox across many exec calls
rather than creating a fresh one per snippet. pip's wheel cache lives on the sandbox disk,
so repeated installs within one sandbox are cheap and across sandboxes are not.
Memory and the OOM killer
A pandas job that exceeds the sandbox's memory gets killed by the kernel, which surfaces as exit code 137
with little or nothing on stderr. If you see 137, the answer is a larger size rather than a code fix:
small gives 2 vCPU and 4 GB, medium gives 4 vCPU and 8 GB, large gives 8 vCPU and 16 GB.
What isolation does and does not cover
The VM boundary protects your infrastructure from the code. It does not protect data you deliberately put
inside the sandbox: the machine has ordinary outbound internet access, so a script that is allowed to read
your credentials is also able to send them somewhere. Pass only task-scoped, short-lived secrets, and pass
them per command through env. See secure code execution
for the full threat model.
Questions
Why not use RestrictedPython or a custom __builtins__?
Because those are not security boundaries, and their own authors say so. RestrictedPython describes itself as a tool for defining a restricted subset of Python inside a trusted environment, not as a sandbox. The stronger attempt, pysandbox, was withdrawn by its author with the explanation that it is broken by design: Python's introspection surface is large enough that escaping a namespace restriction is a puzzle, not a barrier. The conclusion the Python community reached is to put the sandbox between the interpreter and the OS, not inside the interpreter.
Can I pip install anything?
Yes. The sandbox has an outbound network connection and a working compiler toolchain, so packages with C extensions build rather than failing on a missing gcc. Install into a virtualenv or with pip install --user, since commands run as an unprivileged user. Large scientific packages take real time to install, which is the main argument for creating one sandbox per job rather than one per command.
How do I get a plot or a file back out?
Write it inside the sandbox, then move it across a channel that survives the sandbox. The simplest is base64 through stdout for anything small: python3 -c "import base64,sys;sys.stdout.write(base64.b64encode(open('plot.png','rb').read()).decode())". For anything large, upload from inside the sandbox to your own object storage using a short-lived signed URL passed in through env.
Does matplotlib work without a display?
Yes, with the non-interactive backend. Set MPLBACKEND=Agg in the env of the exec call, or call matplotlib.use("Agg") before importing pyplot. Otherwise the first plotting call tries to find a GUI toolkit and fails in a way that is confusing to a model reading the traceback.
Which Python version is installed?
The Python 3 that ships with the current Ubuntu LTS base. If you need a specific version, install it in the sandbox — uv python install 3.12 or a deadsnakes-style install both work, and pinning the version explicitly in your setup command is good practice anyway if reproducibility matters to you.
Is the interpreter restricted in any way?
No. It is an ordinary CPython on an ordinary Linux machine — sockets, threads, subprocesses and the filesystem all behave normally. That is the point: restricting the interpreter is what does not work. The restrictions live outside it, in the fact that the machine is yours alone, is destroyed on expiry, runs your code unprivileged, cannot reach the cloud metadata endpoint and cannot send mail.
Run your first script
A real interpreter on a machine that belongs to nobody else, for a few cents an hour.