Install Python packages inside a sandbox on the fly
Install Python packages inside a sandbox on the fly with pip or uv — via exec or a code-interpreter kernel — and reuse them in the next run.
An agent reaches for a library that isn't baked into the template — requests, beautifulsoup4, some niche scientific package the model decided it needs mid-task. You don't want to re-bake a template for every one-off dependency. The fix is to install Python packages inside a sandbox on the fly at runtime: run pip (or uv) over the live VM, then use the package in the very next call. The code-interpreter template ships pip and uv and already bakes the common data stack (IPython, pandas, numpy, matplotlib, plotly, kaleido), so most of the time you're only adding extras.
Setup
export PANDASTACK_API_KEY=pds_...
pip install pandastackThe SDK reads PANDASTACK_API_KEY from the environment — never hardcode it. Everything below targets the code-interpreter template, which has pip, uv, and an IPython kernel preinstalled.
Option 1: install via exec
The simplest path is a shell command over the sandbox. exec runs a one-shot process in the guest and returns stdout/stderr/exit code — exactly what pip install produces.
from pandastack import Sandbox
sandbox = Sandbox.create(template="code-interpreter", ttl_seconds=3600)
# Install at runtime. check=True raises CommandFailed if pip exits non-zero.
sandbox.exec("pip install requests beautifulsoup4", check=True)
# Use it in the next call — the package is now on disk in this VM.
out = sandbox.exec("python -c 'import requests; print(requests.__version__)'")
print(out.stdout) # -> e.g. "2.32.3"import { Sandbox } from "@pandastack/sdk";
const sandbox = await Sandbox.create({ template: "code-interpreter", ttlSeconds: 3600 });
await sandbox.exec("pip install requests beautifulsoup4");
const out = await sandbox.exec("python -c 'import requests; print(requests.__version__)'");
console.log(out.stdout); // -> e.g. "2.32.3"uv is faster for cold installs — sandbox.exec("uv pip install --system polars") works the same way and is worth it when an agent installs frequently. For a long install, stream the log instead of blocking; see Stream code output.
Option 2: install from inside the kernel (!pip / %pip)
If you're already driving a stateful code context — a Jupyter-style kernel where variables and imports persist across run_code calls — install inside the kernel using IPython's shell escape (!pip) or magic (%pip). Then import it in the next cell; the kernel state carries over.
sandbox = Sandbox.create(template="code-interpreter", ttl_seconds=3600)
ctx = sandbox.create_code_context() # persistent kernel
# Cell 1: install on the fly. %pip is the IPython magic; !pip also works.
ctx.run_code("%pip install httpx")
# Cell 2 (same kernel, later turn): import and use — state persisted.
ex = ctx.run_code("import httpx; r = httpx.get('https://example.com'); r.status_code")
print(ex.text) # -> "200"const sandbox = await Sandbox.create({ template: "code-interpreter", ttlSeconds: 3600 });
const ctx = await sandbox.createCodeContext(); // persistent kernel
await ctx.runCode("%pip install httpx");
const ex = await ctx.runCode("import httpx; r = httpx.get('https://example.com'); r.status_code");
console.log(ex.text); // -> "200"%pip is the recommended form inside IPython — it installs into the kernel's own interpreter, so the import resolves without a kernel restart. This is the natural shape for an agent loop: the model emits one cell that installs, the next cell that uses it, and the kernel keeps both in the same session.
Which one should an agent use?
| You're using | Install with | Then |
|---|---|---|
One-shot exec / run_code per call | sandbox.exec("pip install <pkg>") | the package is on disk for any later call in this VM |
A persistent CodeContext kernel | ctx.run_code("%pip install <pkg>") | import in the next cell; in-kernel state persists |
| Lots of installs, latency-sensitive | uv pip install --system <pkg> | same as above, faster resolver |
What's already baked (don't reinstall it)
The code-interpreter template bakes the libraries most code-execution agents reach for, so you only install the long tail. Reinstalling a baked package wastes seconds per turn for nothing.
Already in code-interpreter | Notes |
|---|---|
ipython | the kernel behind create_code_context() |
pandas, numpy | DataFrames auto-render as HTML via ex.results[].html |
matplotlib, plotly, kaleido | charts auto-captured as ex.png — no savefig |
pip, uv | the installers themselves |
A quick guard before installing: import importlib.util; importlib.util.find_spec("pandas") is truthy when the package is present. For an agent tool, prefer a try: import X / except ImportError: install wrapper so the model only pays for installs it actually needs.
Honest note: runtime installs are ephemeral
Anything you install at runtime lives in that sandbox only. Kill the VM (or let its TTL reap it) and the package is gone — a fresh Sandbox.create() is back to the baked baseline. That's the right default for one-off, model-authored dependencies: clean slate every time, no drift. But it means three things to be deliberate about:
- It's not free per turn. Cold-installing the same package on every create adds seconds to each task. If an agent always needs
requests, paying for it every turn is waste. - It's not shared across sandboxes. A package installed in one fork or one session doesn't appear in another.
- It doesn't survive a fresh boot. Restart your orchestrator and create a new sandbox, and you reinstall.
You have two ways to make a runtime install stick:
- Snapshot the warmed sandbox. Install once, then
snapshot()— the snapshot captures the full VM (memory + disk), so the installed package comes back withSandbox.create(from_snapshot=id). Ideal for "install once per conversation, resume across turns." See Snapshot a sandbox between agent turns. - Bake a custom template. If a dependency is always needed, put it in the template's Dockerfile and bake it in. Then every
createstarts with it preinstalled — no runtime cost, no drift. See Templates.
Rule of thumb: runtime install for the unpredictable long tail an agent decides it needs; snapshot to keep a runtime install warm within a session; bake for a permanent, every-sandbox dependency.
# Install once, snapshot, and the dep is baked into the resume.
sandbox = Sandbox.create(template="code-interpreter", ttl_seconds=3600)
sandbox.exec("pip install pdfplumber", check=True)
snap_id = sandbox.snapshot() # captures the installed package + VM state
# Later / new process: pdfplumber is already there, no reinstall.
resumed = Sandbox.create(from_snapshot=snap_id)
print(resumed.exec("python -c \"import pdfplumber; print('ready')\"").stdout) # -> "ready"const sandbox = await Sandbox.create({ template: "code-interpreter", ttlSeconds: 3600 });
await sandbox.exec("pip install pdfplumber");
const snapId = (await sandbox.snapshot()).id;
const resumed = await Sandbox.create({ fromSnapshot: snapId });
console.log((await resumed.exec("python -c \"import pdfplumber; print('ready')\"")).stdout); // -> "ready"Next steps
- Code interpreter — the pillar guide: code contexts, rich results, one-shot exec, and file uploads.
- Snapshot a sandbox between agent turns — keep a runtime install warm across turns and restarts.
- Templates — bake a permanent dependency into a custom template instead of installing at runtime.
- Stream code output — follow a long
pip/uvinstall line-by-line instead of blocking.
Stream Code Execution Output in Real Time
Stream code execution output in real time over SSE — pass on_stdout/on_stderr callbacks to exec_stream and get a build's output as it's produced.
Snapshot & restore sandbox state between agent turns
Snapshot sandbox state between agent turns so a paused agent resumes instantly — kernel, memory, and rootfs intact — without redoing setup.