# Python SDK (/docs/python)



The `crownest` package is the official Python client for CrowNest. It
ships sync and async clients with the same surface, snake\_case keyword
arguments, and automatic idempotency keys for create operations. It
requires Python 3.11 or later and depends on `httpx`.

## Install [#install]

Install from PyPI with pip or uv.

```bash title="Terminal"
pip install crownest
# or
uv add crownest
```

## Create a client [#create-a-client]

Construct a `CrowNest` client. All arguments are optional.

```python
from crownest import CrowNest

client = CrowNest(
    api_key="cn_live_...",              # or env CROWNEST_API_KEY
    base_url="https://api.crownest.dev", # default
    timeout=660,                         # seconds
)
```

| Argument   | Default                    | Description                 |
| ---------- | -------------------------- | --------------------------- |
| `api_key`  | `CROWNEST_API_KEY` env var | Org-scoped API key.         |
| `base_url` | `https://api.crownest.dev` | API base URL.               |
| `timeout`  | `660`                      | Request timeout in seconds. |

The sync client works as a context manager and also exposes `.close()`
for manual cleanup:

```python
with CrowNest() as client:
    sandboxes = client.sandboxes.list()
    print(sandboxes["data"])
```

## Async client [#async-client]

`AsyncCrowNest` exposes the same surface with `await` and works as an
async context manager.

```python
import asyncio
from crownest import AsyncCrowNest

async def main() -> None:
    async with AsyncCrowNest() as client:
        sandbox = await client.sandboxes.create(template="python-node")
        result = await sandbox.commands.run("python3 -c 'print(40 + 2)'")
        print(result["exitCode"], result["stdout"])
        await sandbox.kill()

asyncio.run(main())
```

Every sync method has an async counterpart with the same name and
arguments.

## Error handling [#error-handling]

Failed API calls raise `crownest.CrowNestApiError`, which carries
`.status` (HTTP status), `.code` (stable snake\_case error code), and
`.details`. `str(e)` gives the human-readable message.

```python
from crownest import CrowNest, CrowNestApiError

with CrowNest() as client:
    try:
        client.sandboxes.get("sbx_does_not_exist")
    except CrowNestApiError as e:
        print(e.status, e.code, e)
```

<Callout type="info" title="Note">
  A command finishing with a non-zero exit code is not an API error. `commands.run`
  returns normally; check the result's `exitCode` and `status`.
</Callout>

## SandboxHandle ergonomics [#sandboxhandle-ergonomics]

`client.sandboxes.create()` and `.get()` return a `SandboxHandle`. The
handle behaves like the sandbox object — it supports attribute access
(`sandbox.id`, `sandbox.status`), mapping access (`sandbox["expiresAt"]`),
and `.to_dict()` — and adds:

* `.kill()` — destroy the sandbox.
* `.wait_until_ready()` — poll until the sandbox can accept work.
* `.commands`, `.files`, `.artifacts`, `.previews` — sub-clients bound to
  the sandbox, so you don't pass `sandbox_id` on every call. Resource-id
  helpers such as command logs, Artifact downloads, and Preview revoke are
  available on the handle sub-clients too.

```python
sandbox = client.sandboxes.create(template="python-node")
sandbox.files.write("app.js", "console.log('hi')")
result = sandbox.commands.run("node app.js")
```

## Worked example [#worked-example]

This script creates a Python sandbox, writes a script, runs it, exports
the output as an artifact, and cleans up.

```python
from crownest import CrowNest

SCRIPT = """
with open("primes.txt", "w") as f:
    primes = [n for n in range(2, 50)
              if all(n % d for d in range(2, n))]
    f.write("\\n".join(map(str, primes)))
print("wrote", "primes.txt")
"""

with CrowNest() as client:
    sandbox = client.sandboxes.create(
        template="python-node",
        ttl_ms=10 * 60 * 1000,
        metadata={"demo": "primes"},
    )
    try:
        sandbox.files.write("primes.py", SCRIPT)
        result = sandbox.commands.run("python3 primes.py")
        print("exit code:", result["exitCode"])
        print(result["stdout"])

        artifact = sandbox.artifacts.create(path="primes.txt")
        data = client.artifacts.download(artifact["id"])
        print(data.decode())
    finally:
        sandbox.kill()
```

Idempotency keys are generated automatically for `sandboxes.create`,
`sandboxes.extend`, `commands.run`, `commands.start`, and
`artifacts.create`, so safe retries work out of the box. Pass
`idempotency_key=` explicitly to control the key yourself.

## Next steps [#next-steps]

* [Python API reference](/docs/python/reference) — every client and
  method.
* [Sandboxes concept](/docs/concepts/sandboxes) — lifecycle, TTL, and
  templates.
* [API error reference](/docs/api/errors) — error codes in depth.
