---
title: Python SDK
description: The ibby package for Python 3.13 and later. The same calls as the TypeScript SDK, in snake_case.
---

```sh
pip install ibby
```

The package needs Python 3.13 or later. Request and response shapes are the API's own, as pydantic models.

## Make a client

```python
import os

from ibby import Ibby

client = Ibby(
    os.environ["IBBY_API_KEY"],
    # Optional, shown with their defaults:
    max_retries=2,
    timeout=150.0,
)
client.close()
```

Use the client in a `with` block, or call `close()` when you are done. `max_retries` is how many times a call is retried after a dropped connection or a `503`. Set it to `0` to turn retries off. A command is never retried on its own. `timeout` is the most one read or write may wait; `None` turns it off.

The key is read once. Surrounding spaces and line breaks are ignored; a key with a space or a control character inside raises `ValueError`, without showing the key.

## Sandboxes

`create()` returns a `Sandbox`. Its methods act on that sandbox.

```python
import os

from ibby import Ibby

with Ibby(os.environ["IBBY_API_KEY"]) as client:
    sandbox = client.create(
        workdir="/work",
        name="report-run-1",
        lifetime_ms=60_000,
        resources={"cpu": 0.5, "memory_mib": 512},
    )
    print(sandbox.id, sandbox.info.status, sandbox.info.resources)

    sandbox.make_directories(["/work/in", "/work/out"])
    sandbox.write_files({"/work/in/data.csv": "a,b\n1,2\n", "/work/in/logo.bin": b"\x00\x01\x02"})

    for entry in sandbox.list_files("/work/in").entries:
        print(entry.type, entry.size_bytes, entry.path)

    print(sandbox.read_file("/work/in/data.csv", max_bytes=1024).decode())
    sandbox.remove()
```

To act on a sandbox you made earlier, use `client.sandbox(sandbox_id)`. It makes no network call; its `info` is `None` until you call `refresh()`. `client.get(sandbox_id)` fetches the record as well. To remove a sandbox by its name after a crash, when you no longer have its id, use `client.remove_by_name(name)`. An unknown name is fine.

`create()` also takes `image=` to run on an image you built (see [Images](#images)), and `store={"mount_path": "/work/in", "key_prefix": "runs/1/"}` to attach part of the lane's store folder read-only; the [concepts page](/concepts#a-read-only-store-folder) gives the rules.

A sandbox's `status` and a listing entry's `type` are plain strings, so a value added later never breaks your code's parsing.

## Running commands

```python
import os

from ibby import ExecTimedOut, Ibby

with Ibby(os.environ["IBBY_API_KEY"]) as client:
    sandbox = client.create(workdir="/work")
    result = sandbox.exec(
        ["sh", "-c", "echo $GREETING"],
        cwd="/work",
        env={"GREETING": "hello"},
        timeout_ms=30_000,
    )
    if isinstance(result, ExecTimedOut):
        print("timed out")
    else:
        print(result.exit_code, result.stdout, result.stdout_truncated)
    sandbox.remove()
```

`command` is a list of arguments. A string raises `TypeError`, so `"ls -la"` is never sent as single characters. A timeout is a result, not an exception. `stdout` is `None` when the output is unavailable: when you repeat a finished command with the same `idempotency_key` (the outcome comes back, the output does not), or, in this version, when the command timed out. `stdout_truncated` is `False` whenever `stdout` is `None`.

## Errors

Every failed call raises `IbbyError`.

```python
import os

from ibby import Ibby, IbbyError

with Ibby(os.environ["IBBY_API_KEY"]) as client:
    try:
        client.get("sbx_missing")
    except IbbyError as error:
        print(error.status, error.code, error.message, error.request_id)
```

`code` is the API's stable code, such as `not_found` or `sandbox_stopped`, and `message` is the API's own sentence. When no answer came back at all, `status` is `0` and `code` is `connection_error` or `timeout`. A redirect is never followed: it is an `IbbyError` with code `unexpected_response`. A request that breaks the schema's own rules, such as a relative path, raises pydantic's `ValidationError` before anything is sent. A rule between two fields, such as `store.mount_path` sitting inside `workdir`, is checked by the API, which answers `400 invalid_request`.

## Safe retries

Calls that create or write send a fresh `Idempotency-Key`. To retry one yourself, for example after your process restarts, pass your own key both times:

```python
import os

from ibby import Ibby

with Ibby(os.environ["IBBY_API_KEY"]) as client:
    sandbox = client.create(workdir="/work", idempotency_key="nightly-2026-11-13")
    again = client.create(workdir="/work", idempotency_key="nightly-2026-11-13")
    assert again.id == sandbox.id
    sandbox.remove()
```

## Images

`build_image()` starts a build from a recipe and returns its job. When the job is `done`, its result names the image to create sandboxes from. The [concepts page](/concepts#images) explains recipes. A recipe that breaks the rules, such as a version range, raises pydantic's `ValidationError` before anything is sent. `get_image()` raises `IbbyError` with code `image_not_built` (status 409) when the image has no build on the lane's current provider, which happens only after a lane changes provider; rebuild it from its recipe.

```python
import os
import time

from ibby import Ibby

HASH = "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
recipe = {
    "base": {"reference": "python:3.13-slim", "digest": HASH},
    "packages": [{"name": "numpy", "version": "2.3.1", "hashes": [HASH]}],
    "import_check": ["numpy"],
}

with Ibby(os.environ["IBBY_API_KEY"]) as client:
    job = client.build_image(recipe)
    while job.status in ("accepted", "running"):
        time.sleep(5)
        job = client.get_job(job.id)
    if job.status == "done" and job.result is not None:
        image_id = job.result["image_id"]
        print(client.get_image(image_id).status)
        sandbox = client.create(workdir="/work", image=image_id)
        sandbox.remove()
```

## Jobs

```python
import os

from ibby import Ibby

with Ibby(os.environ["IBBY_API_KEY"]) as client:
    job = client.get_job("job_...")
    if job.status == "running":
        client.cancel_job(job.id)
```
