# Python SDK quickstart (/docs/quickstart/python)



Run the full sandbox lifecycle with the Python SDK: create a sandbox, run a
command, write and read a file, export an artifact, expose a preview, and clean
up. The end-to-end script is at the bottom of this page.

For archive-based repo/test work, use `client.workspace_runs` instead of
creating a sandbox directly. Workspace Runs upload an existing `.tar.gz` or
`.tgz`, run a command, stream execution events, and persist durable evidence:

```python
import hashlib
from pathlib import Path

from crownest import CrowNest

archive = Path("repo.tgz").read_bytes()

with CrowNest() as client:
    run = client.workspace_runs.run_archive(
        archive,
        sha256=hashlib.sha256(archive).hexdigest(),
        size_bytes=len(archive),
        template="python-node",
        command="pytest -q",
    )
```

See [Workspace Runs](/docs/sdk/workspace-runs) for the full SDK lifecycle.

## Prerequisites [#prerequisites]

* Python 3.11 or later. The SDK depends on `httpx`.
* A CrowNest API key, created in the dashboard at
  [https://crownest.dev](https://crownest.dev). See
  [API keys](/docs/concepts/api-keys) for scopes and presets.

<Callout type="warn" title="Warning">
  The raw API key is shown once, at creation time. Copy it immediately and store it
  somewhere safe — you can't view it again later.
</Callout>

## Set up [#set-up]

1. Install the package from PyPI.

   ```bash title="Terminal"
   pip install crownest
   ```

   With uv: `uv add crownest`.

2. Export your bearer credential. The client reads `CROWNEST_BEARER_TOKEN`
   from the environment, with `CROWNEST_API_KEY` still supported for
   developer API keys.

   ```bash title="Terminal"
   export CROWNEST_BEARER_TOKEN="cn_live_..."
   ```

3. Create a client. The sync client supports use as a context manager,
   which closes the underlying connection for you; outside a `with` block,
   call `.close()` when you're done.

   ```python title="main.py"
   from crownest import CrowNest

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

   All constructor arguments are optional; `CrowNest()` works on its own
   when `CROWNEST_BEARER_TOKEN` is set.

## Walk through the lifecycle [#walk-through-the-lifecycle]

Each step below builds on the previous one inside the same `with` block.

1. Create a sandbox. `create` returns a `SandboxHandle` — the sandbox
   resource with `.id`, dict-style and attribute access, `.to_dict()`, a
   `.kill()` method, and bound sub-clients (`.commands`, `.files`,
   `.artifacts`, `.previews`, `.code`), so you don't repeat the sandbox ID on every
   call.

   ```python
   with CrowNest() as client:
       sandbox = client.sandboxes.create(template="python-node")
   ```

   `python-node` is the default broad repository runner. Use `node` for a lean
   JavaScript and TypeScript environment. You can also pass `ttl_ms` to request
   a lifetime and `metadata` to attach small string labels — see
   [Sandboxes](/docs/concepts/sandboxes). Idempotency keys are generated
   for you automatically.

2. Run a command. `run` waits for the process to exit and returns the full
   command record, including `exitCode`, `stdout`, and `stderr`.

   ```python
   result = sandbox.commands.run("python3 -c 'print(40 + 2)'")
   print(result["exitCode"], result["stdout"])
   ```

   <Callout type="warn" title="Important">
     A non-zero exit code does not raise. The command completed; your code inside it
     failed. Always check `result["exitCode"]` when the outcome matters.
     `CrowNestApiError` is reserved for API-level failures.
   </Callout>

   **Stream output as it happens.** Use callbacks when you want live stdout
   and stderr while still getting the final Command record from `run`.

   ```python
   import sys

   sandbox.commands.run(
       "python3 -c \"import time; [print('step', i, flush=True) or time.sleep(0.2) for i in range(3)]\"",
       on_stdout=lambda chunk: print(chunk, end=""),
       on_stderr=lambda chunk: print(chunk, end="", file=sys.stderr),
   )
   ```

   **Run code, not just commands.** Use Code Runs for notebook-style
   snippets and rich interpreter outputs. The Python SDK exposes the same
   Sandbox-bound Code helpers with snake\_case option names.

   ```python
   code_result = sandbox.code.run("print(40 + 2)")
   print(code_result["stdout"])
   ```

3. Write and read a file. All file paths are inside `/workspace`.

   ```python
   sandbox.files.write("notes.txt", "hello from crownest")
   content = sandbox.files.read("notes.txt")
   print(content)
   ```

   <Callout type="info" title="Note">
     File APIs are confined to `/workspace`. Paths that resolve outside it through `..`
     segments are rejected with the `path_outside_workspace` error code.
   </Callout>

4. Export an artifact. The workspace disappears with the sandbox, so copy
   anything you want to keep to durable storage with an explicit export.

   ```python
   artifact = sandbox.artifacts.create(path="notes.txt")
   print(artifact["id"])
   ```

   You can download it later — even after the sandbox is gone — with
   `client.artifacts.download(artifact["id"])`, which returns `bytes`.

5. Expose a preview. When your sandbox hosts an HTTP service, create a
   preview to get an authenticated URL like
   `https://p-a1b2c3.crownest.dev`.

   ```python
   sandbox.commands.start("python3 -m http.server 8000")
   preview_create = sandbox.previews.create(port=8000)
   preview = preview_create["preview"]
   print(preview["url"])
   ```

   `start` launches the server without waiting for it to exit. Previews
   require authentication in v1. Use `auth_mode="token"` when you need a
   browser-openable link; the one-time `previewToken` is returned only from
   the create response. See [Previews](/docs/concepts/previews).

6. Kill the sandbox. This stops billing and releases the environment.
   Sandboxes also expire automatically at their TTL.

   ```python
   sandbox.kill()
   ```

## Handle errors [#handle-errors]

API failures raise `crownest.CrowNestApiError`, which carries the HTTP
status, a stable machine-readable code, and optional details. `str(e)`
gives a readable summary.

```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.details)
        print(str(e))
```

See the [error reference](/docs/api/errors) for the full list of codes.

## Complete script [#complete-script]

The end-to-end version of the steps above.

```python title="main.py"
from crownest import CrowNest

with CrowNest() as client:
    sandbox = client.sandboxes.create(template="python-node")

    result = sandbox.commands.run("python3 -c 'print(40 + 2)'")
    print(result["exitCode"], result["stdout"])

    sandbox.files.write("notes.txt", "hello from crownest")
    content = sandbox.files.read("notes.txt")
    print(content)

    artifact = sandbox.artifacts.create(path="notes.txt")
    print("artifact:", artifact["id"])

    sandbox.commands.start("python3 -m http.server 8000")
    preview_create = sandbox.previews.create(port=8000)
    preview = preview_create["preview"]
    print("preview:", preview["url"])

    sandbox.kill()
```

## Use the async client [#use-the-async-client]

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

```python title="main_async.py"
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())
```

## Next steps [#next-steps]

* Learn how lifetimes, templates, and statuses work in
  [Sandboxes](/docs/concepts/sandboxes).
* Stream logs and collect outputs with [Commands](/docs/concepts/commands).
* Browse every method in the [Python SDK reference](/docs/python).
* Prefer the terminal? Try the [CLI quickstart](/docs/quickstart/cli).
