# How Do I Set Up a Python Dev Environment in a Cloud Sandbox With uv?

> A new sandbox already has Python 3.12.3 and uv 0.13.1. Run uv python install for another version, clone your project, run uv sync --frozen and uv run pytest. Connect VS Code through sbx ssh-config, then save the sandbox as a named snapshot so the next one starts with Python, venv and cache ready.

Source: https://sandboxer.com/guides/python-dev-environment-in-a-cloud-sandbox. Updated 2026-10-27.

**Short answer:** A new sandbox already has Python 3.12.3 and uv 0.13.1. Run uv python install for another version, clone your project, run uv sync --frozen and uv run pytest. Connect VS Code through sbx ssh-config, then save the sandbox as a named snapshot so the next one starts with Python, venv and cache ready.

We ran every command on this page on prod on 2026-10-27, on `small` sandboxes (2 vCPU, 4 GB) in eu-central. The project is [pallets/click](https://github.com/pallets/click) at commit `2247b35`: a real library with a `uv.lock` and about 2,300 tests.

## Results

| Step | Time on `small` |
| - | - |
| `uv python install 3.13` (33.5 MiB download) | 1.0 s |
| `uv sync --frozen`, empty cache (3 runs) | 0.98, **0.98**, 1.02 s |
| `uv sync --frozen`, warm cache, no `.venv` (3 runs) | 0.08, **0.08**, 0.08 s |
| `uv sync --frozen`, nothing to do (3 runs) | **0.01 s** |
| `uv run pytest`, Python 3.13 | 2262 passed in 5.5 s (9.5 s wall) |
| `uv run pytest`, system Python 3.12 | 2262 passed in 4.2 s (7.8 s wall) |
| `sbx new --from` the snapshot, SDK `create()` to ready | 2.6 s |
| In the new sandbox: `uv sync`, as saved / cache only (3 runs each) | **0.01 s** / **0.19 s** |

The lesson: with uv, the install is rarely what you wait for. Our cold sync was about one second. A snapshot still helps because it also holds the extra Python, the clone and anything slow you add later, such as apt packages or a source-built wheel.

## 1. See what the sandbox already has

```bash
sbx new --name pydev-1 --type small
sbx exec sb_bpznbn5i6o bash -lc "python3 --version; which python3 uv; uv --version; uv python list --only-installed; uv cache dir"
```

```text
Python 3.12.3
/usr/bin/python3
/usr/local/bin/uv
uv 0.13.1 (x86_64-unknown-linux-musl)
cpython-3.12.3-linux-x86_64-gnu    /usr/bin/python3.12
cpython-3.12.3-linux-x86_64-gnu    /usr/bin/python3 -> python3.12
/home/user/.cache/uv
```

The system Python is Ubuntu's 3.12.3, and `uv`, `uvx`, `pip` and `pipx` are installed. These are the versions on 2026-10-27; the image changes over time. The full inventory is in [What's pre-installed in a sandbox](/guides/whats-preinstalled-in-a-sandbox).

## 2. Install another Python

```bash
sbx exec sb_bpznbn5i6o --timeout 300 bash -lc "uv python install 3.13 && uv run --python 3.13 --no-project python -c 'import sys; print(sys.version)'"
```

```text
Downloading cpython-3.13.16-linux-x86_64-gnu (download) (33.5MiB)
Installed Python 3.13.16 in 999ms
 + cpython-3.13.16-linux-x86_64-gnu (python3.13)
3.13.16 (main, Oct 14 2026, 01:04:01) [Clang 23.1.3 ]
```

uv puts managed Pythons in `~/.local/share/uv/python` and links `python3.13` into `~/.local/bin`. Both are under `/home/user`, so they are kept by `sbx stop` and saved in a snapshot. With 3.13 installed, uv picked it over the system 3.12 for the project below: uv [prefers its own managed Pythons](https://docs.astral.sh/uv/concepts/python-versions/) by default.

## 3. Sync a real project and run its tests

```bash
sbx exec sb_bpznbn5i6o --timeout 600 bash -lc '
  git clone -q https://github.com/pallets/click && cd click && git checkout -q 2247b35ea1c4
  uv sync --frozen
  uv run --frozen pytest -q -p no:cacheprovider | tail -1'
```

```text
…
 + uv==0.11.3
 + virtualenv==21.2.0
2262 passed, 24 skipped, 31000 deselected, 1 xfailed in 5.47s
```

`--frozen` installs exactly what `uv.lock` pins and fails rather than re-resolving. click's `pyproject.toml` sets `default-groups` (dev, tests, typing, pre-commit), so a plain `uv sync` installs pytest, mypy and pyright too. The 31,000 deselected tests are click's stress tests, which its `addopts` skip by default.

To run the same suite on the system Python without touching the main venv, point uv at a second environment:

```bash
cd ~/click && UV_PROJECT_ENVIRONMENT=.venv-312 uv run --frozen --python 3.12 pytest -q -p no:cacheprovider | tail -1
```

```text
2262 passed, 24 skipped, 31000 deselected, 1 xfailed in 4.24s
```

One detail matters for snapshots. uv hardlinks packages from its cache into `.venv`, so the two share disk. In our sandbox `du` reported 194 MB for `.venv` and 195 MB for the cache on their own, but only 200 MB for both together. Saving both costs about the same as saving one.

## 4. Open it in VS Code

Run `sbx ssh-config --install` once on your laptop. It adds a `Host *.sbx` entry that tunnels SSH through the CLI over HTTPS:

```text
Host *.sbx
  User user
  IdentityFile ~/.ssh/sandboxer_ed25519
  IdentitiesOnly yes
  ProxyCommand sbx proxy %h
  …
```

Then any SSH tool reaches the sandbox by id:

```bash
ssh sb_bpznbn5i6o.sbx 'cd click && uv run --frozen which python && git log -1 --format=%h'
```

```text
/home/user/click/.venv/bin/python
2247b35
```

In VS Code or Cursor, run **Remote-SSH: Connect to Host…**, enter `sb_bpznbn5i6o.sbx`, open `/home/user/click`, and choose `/home/user/click/.venv/bin/python` as the interpreter. The Python extension then runs and debugs tests in the sandbox's venv. We tested the `ssh` command above, not the VS Code UI; the editor setup, Cursor and troubleshooting are in [SSH into a cloud sandbox from VS Code or Cursor](/guides/ssh-into-a-cloud-sandbox-from-vs-code-or-cursor). For a browser-only editor, see [Run code-server](/guides/run-code-server-vs-code-in-the-browser).

## 5. Save it as a named snapshot

```bash
sbx snapshot sb_bpznbn5i6o pydev-uv
```

```text
{"ok":true,"type":"snapshot.named.info","snapshot":{…,"name":"pydev-uv",…,"status":"ready","type":"small",…}}
```

The save took 2.1 seconds. The snapshot holds the whole disk: Python 3.13, the clone, `.venv` and `~/.cache/uv`. You can delete the source sandbox afterwards; the snapshot stays. To version the snapshot by lockfile hash and rebuild it automatically when `uv.lock` changes, follow [Cache dependencies in a named snapshot](/guides/cache-dependencies-in-a-named-snapshot).

## 6. Use the Python SDK from outside and inside

**Outside**, on your laptop or CI runner, `Sandboxer()` reads `SANDBOXER_API_KEY`. This script starts a sandbox from the snapshot, times `uv sync` and the tests, and deletes the sandbox. Install the SDK with `pip install sandboxer-sdk` (the package named `sandboxer` on PyPI is an unrelated project).

```python
import time
from sandboxer import Sandboxer

with Sandboxer() as client:  # reads SANDBOXER_API_KEY
    t = time.monotonic()
    sb = client.create(type="small", from_="pydev-uv", name="pydev-2")
    print(f"{sb.id} ready in {time.monotonic() - t:.1f} s")
    try:
        steps = [
            ("as saved", "cd ~/click && uv sync --frozen -q"),
            ("cache only", "cd ~/click && rm -rf .venv && uv sync --frozen -q"),
            ("tests", "cd ~/click && uv run --frozen pytest -q -p no:cacheprovider | tail -1"),
        ]
        for label, cmd in steps:
            run = sb.exec(["bash", "-lc", f"TIMEFORMAT=%R; {{ time ( {cmd} ) ; }} 2>&1"], timeout_seconds=300)
            out = run.stdout.strip().splitlines()
            print(f"{label}: exit {run.exit_code}, {' | '.join(out[-2:])}")
    finally:
        sb.delete()
        print("deleted", sb.id)
```

Our run repeated the first two steps three times each:

```text
sb_5bp3zm3lue ready in 2.6 s
as saved: exit 0, 0.046
as saved: exit 0, 0.009
as saved: exit 0, 0.008
cache only: exit 0, 0.274
cache only: exit 0, 0.155
cache only: exit 0, 0.192
tests: exit 0, 2262 passed, 24 skipped, 31000 deselected, 1 xfailed in 4.19s | 7.964
deleted sb_5bp3zm3lue
```

The managed Python 3.13 came with the snapshot. Nothing was downloaded, and the tests ran on 3.13 without a reinstall.

**Inside** the sandbox, `SANDBOXER_TOKEN` and `SANDBOXER_API_URL` are already set, so `Sandboxer()` needs no arguments. That token is limited to its own sandbox:

```python
# whoami_sbx.py, run in the sandbox
import os
from sandboxer import Sandboxer, SandboxerError

with Sandboxer() as client:  # reads SANDBOXER_TOKEN and SANDBOXER_API_URL
    me = client.get(os.environ["SANDBOXER_ID"])
    print(me.id, me.name, me.state)
    print(sum(1 for _ in client.list()), "sandbox(es) visible to this token")
    try:
        client.create(type="small", wait=False)
    except SandboxerError as e:
        print(e.status, e.code, e.message)
```

```bash
uv run --no-project --with sandboxer-sdk python ~/whoami_sbx.py
```

```text
sb_bpznbn5i6o pydev-1 ready
1 sandbox(es) visible to this token
403 api_key_action_forbidden This API key does not allow sandbox.create.
```

We ran the lookup and the `create` call as two separate scripts; the output lines are theirs. `uv run --with` fetched the SDK and its 11 dependencies into a throwaway environment in under 2.5 seconds, so it never touches the project's venv.

| | Outside (laptop, CI) | Inside the sandbox |
| - | - | - |
| Key | Your `SANDBOXER_API_KEY` | `SANDBOXER_TOKEN`, set for you |
| Can create, fork, snapshot, delete | Yes, as your key's scope allows | No (403) |
| Can exec, read and write files, host ports | Any sandbox your key reaches | Only its own sandbox |
| Use it for | Orchestrating sandboxes, CI | Code in the sandbox that needs its own id, files or hosted URL |

## FAQ

### Which Python version does a Sandboxer sandbox have?

On 2026-10-27, Ubuntu's Python 3.12.3 at `/usr/bin/python3`, with uv 0.13.1. `uv python install 3.13` added 3.13.16 in about one second. The image is updated over time, so check `python3 --version` in your own sandbox.

### Should I use pip or uv in a sandbox?

Either works. uv is already installed, and in our test it synced click's 32 locked packages in about one second from an empty cache. For a pip comparison on a larger project, see [Cache dependencies in a named snapshot](/guides/cache-dependencies-in-a-named-snapshot).

### Does the venv survive sbx stop and sbx resume?

Yes. The venv, uv's cache and uv-installed Pythons all live under `/home/user`, which is on the sandbox disk. Stop keeps the disk, and a named snapshot or fork copies it.

### Can I make the snapshot a different size than the sandbox I built it in?

A sandbox from a snapshot takes the snapshot's size unless you pass `--type`. Moving to a smaller size isn't supported yet, so build the snapshot on the smallest size you plan to use.

### Can code inside the sandbox start more sandboxes with the SDK?

Not with the built-in `SANDBOXER_TOKEN`. It returned 403 `api_key_action_forbidden` for `create` in our test. Orchestrate from outside with your own API key.

## Related

- [Cache dependencies in a named snapshot](/guides/cache-dependencies-in-a-named-snapshot)
- [SSH into a cloud sandbox from VS Code or Cursor](/guides/ssh-into-a-cloud-sandbox-from-vs-code-or-cursor)
- [What's pre-installed in a sandbox](/guides/whats-preinstalled-in-a-sandbox)
- [Python SDK](/docs/sdk-python)
