Skip to main content

Prized Python SDK: Sandboxes (beta)

A sandbox is a microVM your code creates, runs commands in, moves files in and out of, pauses for the price of its storage, forks into live copies, and deletes. This package is the Python client for the Prized Sandboxes API, with a sync Sandbox and an asyncio AsyncSandbox that share one surface.

Sandboxes are in beta. A workspace on free credit alone runs them on the free tier: up to 10 at once, each up to 2 vCPU and 4 GB of memory and 60 minutes of running. A top-up or a paid plan lifts the limits: a sandbox then runs up to 7 days at a stretch, and a paused one is kept until you delete it.

Install

pip install prized

Python 3.10 or newer. The only dependency is httpx.

Authenticate

Make an API key at prized.dev/dashboard/workspace/api-keys and put it in the environment. New to Prized? Open Sandboxes signs you up (free credit, no card) and makes one.

export PRIZED_API_KEY=dcp_…

Or pass api_key="dcp_…" to any call. The key acts as you and sees every sandbox in its workspace.

Quickstart

from prized import Sandbox

with Sandbox.create() as sb:  # returns once it runs
    result = sb.exec("python3 --version")
    print(result.stdout, result.exit_code)

    sb.files.write("hello.py", "print('hello from the sandbox')\n")
    print(sb.exec("python3 hello.py").stdout)
# leaving the block deletes the sandbox

The same with asyncio:

import asyncio
from prized import AsyncSandbox


async def main() -> None:
    async with await AsyncSandbox.create() as sb:
        result = await sb.exec("node --version")
        print(result.stdout)


asyncio.run(main())

Many at once, a few at a time: a workspace caps how many sandboxes it holds (10 on the free tier), and a sandbox being deleted still counts for a moment, so each one is gone before its slot takes the next command:

async def run_all(commands: list[str], limit: int = 4) -> list[str]:
    slots = asyncio.Semaphore(limit)  # 4 fits the free tier

    async def one(cmd: str) -> str:
        async with slots:
            async with await AsyncSandbox.create(vcpu=2, memory_gb=4, labels={"job": "batch"}) as sb:
                try:
                    result = await sb.exec(cmd, timeout=600)  # past it: CommandTimeoutError
                finally:
                    await sb.delete(wait=True)
                if result.exit_code != 0:
                    raise RuntimeError(f"{cmd!r} failed: {result.stderr}")
                return result.stdout

    # Every job settles (and deletes its sandbox) before a failure is raised.
    results = await asyncio.gather(*(one(c) for c in commands), return_exceptions=True)
    for r in results:
        if isinstance(r, BaseException):
            raise r
    return results

Reference

Everything below exists on both classes; on AsyncSandbox every method and classmethod is awaited.

Sandbox.create(template="prized-base-v1", **options) -> Sandbox

Creates a sandbox and returns once it is running, ready for commands.

Argument Meaning
template prized-base-v1 (Ubuntu with Node, Python 3.12 and git; Python has only its standard library, and packages go in a venv: python3 -m venv ~/.venv && ~/.venv/bin/pip install numpy) or prized-agent-v1 (adds Claude Code and Codex). Sandbox.templates() lists them with their shapes.
vcpu, memory_gb or memory_mb, disk_gb The shape. Each defaults to the template's; the template sets the bounds (up to 32 vCPU, 128 GB of memory and 128 GB of disk; 2 vCPU and 4 GB on the free tier).
env_vars dict[str, str] set for every command in the sandbox.
labels dict[str, str] to find it by later (Sandbox.list(labels=...)).
auto_pause_min Idle minutes before it pauses itself. Commands and file transfers count as activity. 0 turns it off.
auto_delete_min Minutes after pausing before it is deleted. 0 turns it off.
max_runtime_min Its run time (below): minutes of running before it is deleted. 24 hours by default, up to 7 days (10 080); 60 on the free tier.
timeout Seconds for the request, its retries and the wait for running (default 180). A large shape (more than 16 vCPU or about 58 GB of memory) can take a minute or two to start: give it timeout=300 or more.
wait False returns as soon as the API accepts (state starting), still within timeout.
idempotency_key Makes a retried create return the same sandbox. The SDK sends a fresh one per call when you don't, so its own retries never make a second sandbox.

If the sandbox fails to start or timeout runs out, the SDK raises PrizedError (code sandbox_error or timeout; sandbox_id set) and deletes the sandbox. A create that is interrupted (Ctrl-C, a cancelled task) or never gets an answer (timeout or network_error once its retries are spent) finds the sandbox it may have made and deletes it too.

With your own idempotency_key, the SDK deletes nothing: that key may be shared with another call holding the same sandbox. The error names the sandbox (sandbox_id); resend the create with the key to get it back, or delete it. fork() follows the same rules for its copies.

That cleanup is best effort, bounded (10 seconds for a create, a little more per copy for a fork): if Prized cannot be reached meanwhile, a sandbox may be left until its run time ends, or, if it pauses first (auto_pause_min), until you delete it (free tier: 30 days after its last use). Label your sandboxes, and Sandbox.delete_by_labels(...) deletes any leftovers once it can.

Create, pause() and resume() ask the API to answer once the change has landed, and only poll if it has not within a few seconds.

Every classmethod also takes the connection options:

Option Default
api_key PRIZED_API_KEY
base_url https://api.prized.dev
edge_url The command endpoint the API names for each sandbox
http_client A client the SDK owns. Pass your own httpx.Client / httpx.AsyncClient for proxies or custom transports; the SDK never closes it.
max_retries 3
poll_outage 120 seconds: how long a streamed or long exec keeps polling through a dropped connection before it stops the command

Find sandboxes

sb = Sandbox.connect("sbx_…")  # any state; no waiting
mine = Sandbox.list(labels={"job": "batch"}, state="running")
templates = Sandbox.templates()  # [Template(id, defaults, min, max, ...)]

list() returns the workspace's sandboxes that are not yet deleted, newest first. labels keeps those carrying every label; state takes one state or several.

Delete by label

ids = Sandbox.delete_by_labels({"job": "batch"})  # every sandbox carrying it, in one call

delete_by_labels() needs at least one label and returns the ids it deleted. It is safe to repeat (one already deleting is listed again; none matching returns []). A sandbox being deleted still counts toward your quota until Sandbox.list(labels=...) no longer returns it.

Fields

sb.id (sbx_…), sb.state, sb.template, sb.resources (vcpu, memory_mb, memory_gb, disk_gb), sb.labels, sb.auto_pause_min, sb.auto_delete_min, sb.expires_at (when its run time ends; None while a paused sandbox's clock is stopped), sb.created_at, sb.last_active_at, sb.error_reason, and sb.info (all of it; sb.info.raw is the document as the API sent it).

States: starting, running, pausing, paused, resizing, deleting, deleted, error. Fields reflect the last answer the SDK read; sb.refresh() reads the sandbox again.

sb.exec(cmd, cwd=None, env=None, timeout=None, on_stdout=None, on_stderr=None, background=False, full_output=False, poll_outage=None)

Runs one command with bash as the user prized and returns ExecResult(stdout, stderr, exit_code, truncated, timed_out, duration_ms).

  • cmd is a shell string ("ls -la | wc -l") or a list of arguments (["python3", "-c", code]), which the SDK quotes for you.
  • cwd defaults to /home/prized/work; relative paths resolve against it.
  • env adds variables for this command, on top of the sandbox's env_vars.
  • timeout is in seconds: default 60, 0 for no limit. A command that runs past it is killed, and the call raises prized.CommandTimeoutError (a PrizedError, code command_timeout). Its result is the ExecResult with what the command printed before (also err.stdout, err.stderr), exit_code=-1 and timed_out=True.
  • A non-zero exit code is a result, not an exception.
  • One call keeps the first 4 MiB of stdout and of stderr, and truncated=True says when it cut one. Pass full_output=True for builds, test suites and anything else whose output you need whole: the result keeps up to 100 MiB of each stream (truncated=True past that). It is still one request when the output is small; past 4 MiB the SDK reads it back from files in /home/prized/.prized/exec/, then removes them. With it, a command a signal ends gets exit_code 128+n (a plain exec may give -1), as a background command does.
  • on_stdout / on_stderr receive the output as it arrives. On AsyncSandbox they may be coroutine functions.
  • A streamed command (or one with a timeout over 240 seconds, or 0) rides out a dropped connection: the SDK keeps polling it for up to poll_outage seconds (default 120, set on the call or the client) before it gives up and stops the command; a refused key, or an answer that settles it (the sandbox deleted or not running), stops it at once.
result = sb.exec("npm test", timeout=180, full_output=True)  # every line of the log
sb.exec(
    "python3 -m venv .venv && .venv/bin/pip install -r requirements.txt",
    timeout=600,
    on_stdout=lambda out: print(out, end=""),
)  # chunks keep their own newlines

Background commands

background=True starts the command and returns a CommandHandle at once. The command runs in its own session: it keeps running after your process exits, and while it runs, kill() reaches every process it started (not one that left its process group, such as a daemon). Once the command has ended, kill() returns False and signals nothing.

server = sb.exec("python3 -m http.server 8000", background=True)
server.status()  # CommandStatus(running=True, exit_code=None)
server.logs()  # CommandLogs(stdout=..., stderr=...) so far
server.kill()  # SIGTERM (or kill("SIGKILL")); False if it had exited
result = server.wait(on_stdout=lambda out: print(out, end=""))  # ExecResult once it ends
server.cleanup()  # remove its log files from the sandbox

wait(timeout=...) bounds the wait only; the command keeps running, and wait() returns what the command printed by the time it ended (not what a process it left behind prints after). Pass on_stdout/on_stderr to wait(); exec(..., background=True) refuses them. A background command has no time limit unless you pass timeout= (whole seconds); one that runs past its limit is killed, wait() raises CommandTimeoutError at once, and anything it started that ignores the TERM is killed 5 seconds later. env applies to the command itself. Background and streamed commands run with PYTHONUNBUFFERED=1 unless their environment already sets it (env={"PYTHONUNBUFFERED": ""} keeps Python's buffering), so a Python program's output arrives as it prints. Its output is kept in /home/prized/.prized/runs/ until cleanup(), which removes it once the command has ended (a command still running keeps its files, and its time limit); a command that prints without end fills the sandbox's disk there, so redirect such output yourself (> /dev/null, or a log you rotate). logs() says truncated=True when a stream passed what the SDK keeps.

sb.files

Method Does
write(path, data, mode=None) Writes str (UTF-8) or bytes, creating parent directories and replacing any file there whole. A file it replaces keeps its mode, so a script made executable stays executable; a new file gets the sandbox's default (0600 in prized-base-v1 and prized-agent-v1). mode=0o755 sets the permission bits. Returns WriteInfo(path, size).
read(path) / read_bytes(path) The contents as str / bytes.
upload(local_path, path, mode=None) Copies a local file in, as write does. Returns WriteInfo.
download(path, local_path) Copies a file out (local parent directories are created). Returns the byte count.
list(path="/home/prized/work") The entries of a directory, sorted by name: FileInfo(name, path, size, is_dir, is_symlink, mode, modified).
remove(path, recursive=False) Deletes a file or an empty directory; recursive=True deletes a directory and everything under it. A missing path is not an error.
exists(path) True or False.

Paths are absolute or relative to /home/prized/work. write, read, upload and download work inside /home/prized.

Lifecycle

sb.pause()  # memory and disk kept (until you delete it; free tier: 30 days); no compute charged
sb.resume()  # every process carries on where it left off
kids = sb.fork(4)  # 4 new sandboxes, each a live copy of this one (running or paused)
sb.refresh()  # read the sandbox again (state and the other fields)
sb.keep_alive()  # count as activity now: the auto-pause clock starts over
sb.delete()
sb.close()  # release the SDK's connections; the sandbox keeps running
  • pause() and resume() return once the sandbox is paused / running. Pass wait=False to return as soon as the API accepts, and timeout= (seconds) to bound the whole call (the request, its retries and the wait).
  • A command or file call on a paused sandbox resumes it first (billed and counted as activity like any resume), so resume() is optional before one.
  • fork(count=1, labels=None) works on a running or a paused sandbox. Each copy is an ordinary sandbox with its own id, its own files from then on, and the parent's memory and every process as they were at the fork (or at the pause), running. A running parent pauses briefly, usually under a second, and keeps running; a paused one stays paused. All or nothing: it returns once every copy is running; if any copy fails, or the call runs out of time (timeout=, default 180 s, the request and the wait together), every copy is deleted and PrizedError is raised. A fork that does not fit raises fork_capacity, whose message says how many copies fit now (err.body["fits"]). The copies keep the parent's labels unless you pass labels=, which gives them only the labels you pass.
  • delete() returns once the API accepts; delete(wait=True) waits until the sandbox is gone (a deleting sandbox still counts toward quotas). Deleting a sandbox that is already gone is not an error.
  • with Sandbox.create(...) as sb: deletes the sandbox when the block ends, error or not.

Run time

A sandbox is deleted when its run time ends (sb.expires_at). The clock counts running time only:

  • Paid: 24 hours by default, up to 7 days (max_runtime_min on create). The clock stops while the sandbox is paused, and each resume starts it again with a fresh max_runtime_min. A paused sandbox is kept, memory and disk, until you delete it (or its auto_delete_min does).
  • Free tier: 60 minutes of running in all. Paused time does not count, but a resume carries on the time left. A paused one is kept 30 days after its last use.
  • Forks: on a paid workspace each copy gets the parent's max_runtime_min from the fork; on the free tier copies keep the parent's deletion time.
  • Extend a running one with the CLI (prized sandbox extend <id> --by 2h) or the API (POST /api/v1/sandboxes/{id}/extend; see Run time). On the free tier an extend cannot move the end later.

Errors

Every failure of a call to Prized raises prized.PrizedError. Two kinds keep Python's own exceptions: arguments the SDK refuses before sending anything raise ValueError or TypeError, and files.upload() / files.download() raise OSError for the local file.

Attribute
code Machine-readable: branch on this.
status The HTTP status, or None (network failure, timeout, missing key).
message A sentence for people.
reason A finer reason when the API gives one, e.g. which quota.
sandbox_id The sandbox concerned, when there is one.
body The API's whole error document.
from prized import PrizedError, Sandbox

try:
    sb = Sandbox.create()
except PrizedError as err:
    if err.code in ("paid_balance_required", "insufficient_balance"):
        print("Add funds at prized.dev, then retry.")
    elif err.code == "sandbox_quota_exceeded":
        print(f"At a workspace quota ({err.reason}).")
    else:
        raise

Codes you may meet:

Code Means
missing_api_key No api_key= and no PRIZED_API_KEY.
invalid_api_key The key has characters no key has (a stray newline?); it is never quoted back.
edge_unknown A base_url other than Prized's, whose API named no command endpoint on its own host: pass edge_url=.
unauthorized, token_expired The key was refused.
key_access_denied The key lacks the Sandboxes permission.
paid_balance_required The workspace has only free credit, and sandboxes need a paid balance right now; add funds.
insufficient_balance The balance is empty.
sandbox_quota_exceeded A workspace quota is full; reason names it. On the free tier the body says "tier": "free" and gives limit in the reason's unit (err.body["limit"]): a top-up lifts it.
template_not_found, invalid_resources Check Sandbox.templates(). invalid_resources with "tier": "free" is past the free tier's 2 vCPU / 4 GB.
not_found No such sandbox (or file) for this key. For a sandbox, most often one deleted when its run time ended; the message names it.
sandbox_not_running The sandbox is not running and the call could not wake it: it is resizing (refresh() until it leaves resizing, then retry), failed, or being deleted.
sandbox_not_ready A paused or starting sandbox did not reach running within the edge's 10-second wait. Nothing ran; the SDK retries it.
sandbox_unreachable The sandbox did not answer. A command may or may not have run; the SDK does not retry it.
sandbox_deleted The sandbox was deleted: a wait found it gone, a command or file call was sent to it before or while it was deleted (410; a command that was running stopped with it), or pause() or resume() was called while it was being deleted (410). Not retried.
parent_not_running Only a running or paused sandbox can be forked; fork one that is starting, pausing or resizing once it settles.
parent_not_forkable This sandbox cannot be forked. reason resized_while_paused: it has no memory to copy; resume it, then fork it. reason parent_too_big: its machine cannot hold even one copy beside it; fork a smaller sandbox.
fork_capacity The parent's machine has room for only fits more copies (err.body["fits"]): fork that many or fewer, try again later, or create a new sandbox and fork that one, keeping the parent until it runs. Each copy counts toward your quota.
sandbox_capacity No sandbox capacity is free right now. Nothing was created or billed; the SDK retries it after Retry-After.
sandboxes_disabled, sandbox_create_disabled, sandbox_fork_disabled Sandboxes, or creating or forking them, are paused for maintenance. Nothing was created; retry later (the SDK does not).
sandbox_error The sandbox failed; reason says why.
command_failed A file or background-command helper failed in the sandbox.
command_timeout A command ran past its own timeout= and was killed. Raised as CommandTimeoutError, whose result has what it printed.
timeout A wait, or a call, ran out of time. One that ended because a retry could not start in time says what the last attempt met (also in __cause__).
network_error No answer: the API or the sandbox could not be reached, or the connection dropped.
http_error An answer the SDK could not read as an API error (a proxy's page).
bad_response A 2xx the SDK could not read. For a command or file write it did happen, and the message says so.

Retries. The SDK retries rate limits (429, honouring Retry-After), 502, 503 and 504 answers, dropped connections, and every answer a command is retried on, with backoff (0.5 s, 1 s, 2 s, …), up to max_retries times. A read (Sandbox.list, connect, refresh, templates, a file download, and the polling while create, pause, resume, fork or delete waits) is retried on any other 500 too; a write is not, since it may already have happened. A command, a files.write / files.upload, a files.remove or a kill() is sent again only when it provably never arrived: the connection never opened, or the answer says so (rate limited, the sandbox not ready). One whose answer was lost is not sent again: the command may have run, or the sandbox written the file since. Quota, balance and idempotency refusals, and the maintenance switches above, are never retried.

Did it run? When a command, a file write, a files.remove or a kill() fails without a plain refusal, the error keeps its code and its message says which of three it was. It never reached the sandbox (the connection never opened, or the answer proves it, such as sandbox_not_ready): it had no effect. No clear answer came back (a dropped connection, a timeout, or any other 5xx): it may or may not have run, and the message says how to check (read the file back, files.exists(), status()). A 2xx came back that could not be read: it ran. A background command (or a streamed or long exec, which runs as one) whose start got no clear answer is stopped by the SDK, and the error says so, or, when the SDK could not confirm the stop, that it may still be running and under which run id. So is a streamed or long exec whose polling fails while it runs (a dropped connection or a 5xx for longer than poll_outage, a refused key): the error keeps the poll's code and status, is raised from the poll's own error (__cause__), and says whether the SDK stopped the command or it may still be running. An error that settles it (the sandbox deleted or not running, the run not found) is raised as it came.

Deadlines. Retries stay within a call's bound: timeout= for create, pause, resume, fork and delete (the request, its retries and the wait, its polls included; with wait=False too), 2 minutes for connect, list, templates, refresh and keep_alive, and 10 minutes for a file transfer. Retry-After is honoured up to a minute. A retry that could not start in time is not sent: the call raises timeout. A wait's last poll goes out just before its deadline.

Limits

Shape Up to 32 vCPU, 128 GB memory and 128 GB disk per sandbox, within the template's bounds (Sandbox.templates()); 2 vCPU and 4 GB on the free tier.
Command output One call keeps the first 4 MiB of stdout and of stderr (past that, truncated=True); with full_output=True it keeps up to 100 MiB of each. Streamed and background commands read all of it, and their result keeps the first 64 Mi characters of each stream (truncated=True past that; on_stdout/on_stderr still get every chunk).
Command time 60 s by default; timeout=0 for none.
File transfer 100 MiB per file, inside /home/prized.
Fork 1 to 256 copies per call, subject to capacity.
Run time Paid: 24 hours of running by default, up to 7 days; the clock stops while paused and restarts on resume. Free tier: 60 minutes of running in all.
Paused Kept until you delete it; on the free tier, 30 days after its last use.
Workspace Quotas cap sandboxes and their total vCPU, memory and disk. The free tier: 10 sandboxes at once.

Pricing is per second while running, and storage only while paused: prized.dev/pricing.

Not yet: an interactive terminal from the SDK, custom templates, more than 32 vCPU or 128 GB, GPUs.

Development

cd sdk/python
python -m venv .venv && .venv/bin/pip install -e '.[dev]'
.venv/bin/ruff check . && .venv/bin/ruff format --check . && .venv/bin/mypy && .venv/bin/pytest

The tests run against an in-memory fake of the API (tests/fake_api.py) and never touch the network. scripts/package_check.py builds the wheel and sdist and checks them as PyPI gets them. scripts/live_smoke.py drives real sandboxes end to end with PRIZED_API_KEY; it costs money and is not run in CI.

License

Apache-2.0. The full text is in the LICENSE file that ships with the package.

Metadata

Release files for prized 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for prized 0.1.0
File Size Uploaded Provenance
prized-0.1.0.tar.gz 80.7 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for prized 0.1.0
File Interpreter ABI Platform
prized-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 168.0 kB

Release files / prized-0.1.0.tar.gz

Download URL prized-0.1.0.tar.gz
Size 80.7 kB
Tags Source
SHA-256 checksum
How to use checksums
28532e65acdf83493ce4f3c28573f08d3fa3a8da899786e9957895a6a4d84c8c
BLAKE2b-256 checksum
How to use checksums
3723e69de3be406094b25dc415e8bf7a489f7daa265f7f561edbaa00fc15a141
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 26, 2026.

Transparency log

Release files / prized-0.1.0-py3-none-any.whl

Download URL prized-0.1.0-py3-none-any.whl
Size 87.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
574405b338f82c3316943e8a6aa8eba523d92be0b6d50b1501de60dadb5de5c1
BLAKE2b-256 checksum
How to use checksums
0da4bfe0f67cf61fbf5481a332cbc605c7af2dd78b73b2caa586b17772775edd
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 26, 2026.

Transparency log

Security for prized 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for prized-0.1.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 26, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page