> ## Documentation Index
> Fetch the complete documentation index at: https://porter-adi-agent-app-creation-eval.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Python Sandbox SDK volumes

> Create, mount, browse, read, and write sandbox volumes with the Python Sandbox SDK

Volumes provide persistent storage that can be mounted into Porter Sandboxes. Create a volume before launching the sandbox, then pass the volume ID in `volume_mounts`.

<Warning>
  Sandboxes are in a private beta. Please reach out to us at [support@porter.run](mailto:support@porter.run) or over Slack if you are interested in joining.
</Warning>

## Create and mount a volume

```python theme={null}
from porter_sandbox import Porter

with Porter() as porter:
    volume = porter.volumes.create(name="agent-workspace")

    sandbox = porter.sandboxes.create(
        image="python:3.12-slim",
        volume_mounts={"/workspace": volume.id},
    )

    try:
        sandbox.exec(["python", "-c", "open('/workspace/result.txt', 'w').write('done')"])
        result = sandbox.exec(["cat", "/workspace/result.txt"])
        print(result.stdout)
    finally:
        sandbox.terminate()
```

`volume_mounts` is a dictionary keyed by the absolute mount path inside the sandbox. Each value is a volume ID.

## List volumes

`list()` returns volume handles:

```python theme={null}
with Porter() as porter:
    for volume in porter.volumes.list():
        print(volume.id, volume.name, volume.phase, volume.attached_to, volume.path)
```

## Get a volume by name

Volume names are unique within the cluster. Use `get(name)` when you know a volume name:

```python theme={null}
with Porter() as porter:
    volume = porter.volumes.get("agent-workspace")
    print(volume.id)
```

Volume names may contain lowercase letters, numbers, and hyphens, and must start and end with a letter or number. A volume name must be unique within the cluster for the lifetime of the volume. After the volume is deleted, the name can be used again.

## Inspect a volume

```python theme={null}
with Porter() as porter:
    volume = porter.volumes.get("agent-workspace")
    print(volume.name, volume.phase, volume.attached_to, volume.created_at)
```

## Browse volume contents

`listdir` returns the entries directly inside a directory, directories first:

```python theme={null}
with Porter() as porter:
    volume = porter.volumes.get("agent-workspace")

    for file in volume.listdir("/checkpoints"):
        print(file.path, "dir" if file.is_directory else file.size_bytes)
```

`iterdir` walks the whole tree instead, descending into every subdirectory:

```python theme={null}
for file in volume.iterdir("/checkpoints"):
    print(file.path, file.size_bytes)
```

`search` walks the tree and returns only the entries whose name contains a substring, optionally rooted at a subdirectory:

```python theme={null}
configs = volume.search("config.json")
checkpoint_configs = volume.search("config.json", path="/checkpoints")
```

## Read a file

`read_text` returns a whole file as a string, and `read_file` returns raw bytes:

```python theme={null}
config = volume.read_text("/checkpoints/config.json")
data = volume.read_file("/checkpoints/weights.bin")
```

Both accept `offset` and `length` to read part of a file:

```python theme={null}
head = volume.read_text("/logs/train.log", offset=0, length=512)
```

For files too large to hold in memory, `stream` pages through the file in chunks (8 MiB by default):

```python theme={null}
with open("model.safetensors", "wb") as out:
    for chunk in volume.stream("/checkpoints/model.safetensors"):
        out.write(chunk)
```

Reading a path that does not exist raises `NotFoundError`.

## Write a file

`write_text` writes a string and `write_file` writes raw bytes. Both create parent directories as needed and replace any existing file at the path:

```python theme={null}
volume.write_text("/config/app.yaml", "replicas: 3\n")
volume.write_file("/checkpoints/weights.bin", data)
```

A write is atomic: the file appears at its path only after every byte has been written, so an interrupted or failed write leaves the previous contents in place rather than a truncated file. An uploaded file gets the same permissions a sandbox writing to its own mount would leave, so it is indistinguishable from a file the sandbox wrote.

You can write to a volume whether or not a sandbox has it mounted; the volume itself is the target. A single write is capped at 1 GiB; a request made from outside the cluster with an API token must also finish within 30 seconds, so write very large files from an app running in the same cluster.

## Move or rename a file

`move_file` moves a file or directory to a new path. The second argument is the entry's full new path, not a directory to drop it into, so one call both renames and relocates. A directory moves with everything under it:

```python theme={null}
volume.move_file("/notes.txt", "/archive/notes.txt")
volume.move_file("/models/v1", "/models/v2")
```

The destination's parent directory must already exist, and nothing is overwritten. Moving onto a path something already occupies fails and leaves the entry where it was.

Every method here has an async counterpart on `AsyncVolume`, returned by `AsyncPorter`:

```python theme={null}
async with AsyncPorter() as porter:
    volume = await porter.volumes.get("agent-workspace")

    async for file in volume.iterdir("/checkpoints"):
        print(file.path)

    config = await volume.read_text("/checkpoints/config.json")
```

## Access volume data from apps

Volumes live on a shared disk that apps on the same cluster can attach. Attach the disk named `sandbox-volumes` to a service in your app; it mounts at `/data/<app-name>/sandbox-volumes` and contains one subdirectory per volume.

Each volume handle exposes a `path` with the volume's subdirectory on that disk, so an app reads a volume's data at `/data/<app-name>/sandbox-volumes/<path>`:

```python theme={null}
with Porter() as porter:
    volume = porter.volumes.get("agent-workspace")
    print(volume.path)  # sandbox-vol-2f1c8b7e-...
```

The disk is a live view: writes a sandbox makes to its mounted volume are visible to apps right away, and new volumes show up as new subdirectories without redeploying the app.

<Warning>
  Volume contents are written by sandboxed workloads, which are often running untrusted code. Treat anything your app reads from this disk as untrusted input: hostile file contents, names, or sizes can exploit vulnerabilities in the code that processes them. Porter isolates the sandboxes themselves but does not inspect or sanitize what they write, so validating this data before acting on it is your application's responsibility.
</Warning>

## Delete a volume

Delete volumes by name:

```python theme={null}
with Porter() as porter:
    porter.volumes.delete("agent-workspace")
```

Deleting a volume fails while it is attached to a sandbox. Terminate any attached sandboxes before deleting the volume.

## Async usage

```python theme={null}
from porter_sandbox import AsyncPorter

async with AsyncPorter() as porter:
    volume = await porter.volumes.create(name="async-workspace")

    sandbox = await porter.sandboxes.create(
        image="python:3.12-slim",
        volume_mounts={"/workspace": volume.id},
    )

    try:
        result = await sandbox.exec(["sh", "-lc", "echo hello > /workspace/out.txt && cat /workspace/out.txt"])
        print(result.stdout)
    finally:
        await sandbox.terminate()
```

## Next steps

* [Python Sandbox SDK quickstart](/sandboxes/sdk/python/quickstart)
* [Python Sandbox SDK reference](/sandboxes/sdk/python/reference)
* [Python Sandbox SDK errors](/sandboxes/sdk/python/errors)
