# Python SDK

> Read and write rho tables from Python, as DataFrames.

`rho-store` reads and writes tables from Python: the scripts, notebooks and jobs around your agents.

```bash
pip install "rho-store[pandas]"   # or [polars]
export RHO_API_KEY=rho_sk_…       # an API token
```

Make the token on the **API tokens** page of the rho app: see [API tokens](/docs/guides/api-tokens/).

```python
import rho

rho.put(df, "research/competitors")  # a new version; columns keep their types
rho.put(new, "research/competitors", mode="upsert", key="company", message="weekly refresh")
df = rho.get("research/competitors")  # the latest; version=3 for an earlier one
df = rho.query('SELECT category, count(*) AS n FROM "research/competitors" GROUP BY 1')
rho.history("research/competitors")  # who wrote what, when
rho.restore("research/competitors", version=3)  # as a new version
url = rho.share("research/competitors")  # makes it public: a link anyone can open
```

## Data in, data out

`put` takes a pandas or Polars DataFrame, an Arrow table or a list of dicts. `get` and `query` give pandas by default; pass `as_="polars"`, `"arrow"` or `"rows"` for another.

Data travels as Parquet, never JSON, so types come back as they went: whole numbers stay whole (nullable ones become `Int64`), timestamps keep nanoseconds, decimals their scale. A few things are kept differently:

- a time zone becomes UTC, the same instant;
- a category becomes text;
- a DataFrame's default index isn't kept, and a named one becomes a column;
- durations are refused, with a hint.

## Modes

- `replace`, the default: the data becomes the table.
- `append`: adds the rows.
- `upsert`: updates the rows whose `key` matches, and adds the rest.
- `create`: fails if the table exists.

Pass `run="nightly-2026-10-29"` to label a job's writes in the history.

## Async, and other servers

```python
async with rho.AsyncClient() as client:
    await client.put(df, "runs/evals", mode="append", run=run_id)
    df = await client.query('SELECT model, avg(score) FROM "runs/evals" GROUP BY 1')
```

`rho.Client` and `rho.AsyncClient` have the same methods. Pass `api_key=` and `url=` to choose them yourself; `RHO_URL` points the module's functions at another server.

## Warnings and errors

What rho decides about data it's given comes as a `rho.RhoWarning`, and the write is made. Column names are compared without case, so `A` and `a` in one frame are refused.

Errors are `rho.RhoError`, with `kind` (`not_found`, `exists`, `schema_conflict`, …) and a `hint` saying what to do.