PandaStack

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 pandastack

The 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 usingInstall withThen
One-shot exec / run_code per callsandbox.exec("pip install <pkg>")the package is on disk for any later call in this VM
A persistent CodeContext kernelctx.run_code("%pip install <pkg>")import in the next cell; in-kernel state persists
Lots of installs, latency-sensitiveuv 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-interpreterNotes
ipythonthe kernel behind create_code_context()
pandas, numpyDataFrames auto-render as HTML via ex.results[].html
matplotlib, plotly, kaleidocharts auto-captured as ex.png — no savefig
pip, uvthe 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:

  1. Snapshot the warmed sandbox. Install once, then snapshot() — the snapshot captures the full VM (memory + disk), so the installed package comes back with Sandbox.create(from_snapshot=id). Ideal for "install once per conversation, resume across turns." See Snapshot a sandbox between agent turns.
  2. Bake a custom template. If a dependency is always needed, put it in the template's Dockerfile and bake it in. Then every create starts 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

On this page