> ## Documentation Index
> Fetch the complete documentation index at: https://docs.trainy.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Python Panels

> Write a short Streamlit script that reads your run data and render it as a widget on a custom dashboard.

A Python Panel is a [dashboard](/pluto/dashboards) widget whose contents you write yourself in Python. You write a short [Streamlit](https://streamlit.io) script using pandas, matplotlib, seaborn or plotly, preview it against the runs selected in the UI, and save it alongside your charts and media widgets.

Panels run **entirely in your browser**. The script executes in a sandboxed WebAssembly Python runtime ([stlite](https://github.com/whitphx/stlite), which is Streamlit on Pyodide). The server never runs panel code.

## Writing a panel

Open a custom dashboard, click **Edit Dashboard**, then **Add Widget** and choose **Python Panel**. The editor shows your code on the left and a live preview on the right.

* **Run** (or <kbd>Cmd/Ctrl</kbd>+<kbd>Enter</kbd>) executes the draft. Nothing runs as you type.
* The **first** run boots the Python runtime, which takes 10–30 seconds. Later runs reuse it and take about 2 seconds. Changing the **Packages** list starts a fresh runtime.
* **Save** becomes available after one successful run. It stores the code in the dashboard's configuration.
* The **Templates** menu has six starter panels: a metric line chart, a run comparison table, a correlation heatmap, per-run subplots, a logs viewer and a leaderboard. Picking one replaces your draft code and packages, and asks first if you have unsaved edits.

```python theme={null}
import streamlit as st
import mlop

ctx = mlop.get_context()
st.title(f"{ctx.project}: {len(ctx.runs)} runs selected")

df = await mlop.get_metrics(metrics=["train/loss"])
st.line_chart(df, x="step", y="value", color="run_name")
```

## Reading run data

Panels read your data through the built-in `mlop` module. All of its **data methods are async**, so call them with `await`. Top-level `await` works in panel scripts. Run ids are the short string ids you see in URLs.

| Call | Returns | Limits |
| - | - | - |
| `mlop.get_context()` | `Context` (sync) | `.runs` (a list of `Run(id, name, color)` for the runs selected in the UI), `.project`, `.org`, `.theme` (`"light"` or `"dark"`), `.panel` (`.width` and `.height` in px). Reflects the current selection each time you call it. |
| `mlop.colors()` | `{run_id: "#hex"}` (sync) | Run colors, for styling matplotlib figures to match the rest of the dashboard. |
| `await mlop.get_runs(limit=None)` | list of run dicts | The project's runs. `limit` ≤ 200 (default 50). |
| `await mlop.get_metric_names(search=None)` | `[str]` | Distinct metric names. `search` ≤ 500 characters. |
| `await mlop.get_file_log_names(search=None)` | `[{"logName", "logType"}]` | Discover file and media log names. |
| `await mlop.get_metrics(metrics, runs=None, buckets=None)` | long pandas `DataFrame` (`run_id, run_name, metric, step, value`) | ≤ 50 metrics and ≤ 50 runs (defaults to the selected runs). `buckets` 10–2000. Values are averaged within each bucket. |
| `await mlop.get_metric_summaries(metrics, aggregation="LAST", runs=None)` | `{run_id: {metric: value}}` | `aggregation` is one of `LAST`, `AVG`, `MIN`, `MAX`, `VARIANCE`. ≤ 200 metrics and runs. |
| `await mlop.get_metric_values(run_id)` | list of dicts | The latest value of every metric for one run. |
| `await mlop.get_logs(run_id, log_type=None)` | `[{"message", "logType", "time", "lineNumber"}]` | Console logs for one run. |
| `await mlop.get_file_url(run_id, log_name, file_name)` | URL string | A link to a logged file, e.g. for `st.image(url)`. |

A failed call raises `mlop.MlopError` with a `code` of `FORBIDDEN_METHOD`, `BAD_PARAMS`, `UPSTREAM` or `TIMEOUT` (each call times out after 60 seconds). Parameters outside the limits are **rejected, never truncated**.

## Packages

These packages are always available:

* **Preinstalled:** `numpy`, `pandas`, `pillow`, `altair` and their dependencies.
* **Add through the Packages field:** `matplotlib`, `seaborn` (which pulls in matplotlib) and `plotly`. Up to 20 entries.

There is **no PyPI access**. The sandbox blocks all network access except Pluto's own bundled packages, which is also why panels work unchanged on self-hosted and air-gapped deployments. Requesting any other package fails at install time with a clear error.

## Security

* A panel runs in a sandboxed iframe with no access to your cookies, browser storage or the surrounding page, and no network access.
* Data reaches the panel only through the `mlop` calls above. Pluto runs each one as a **read-only** query under **your own session**, scoped to the organization and project you are viewing. A panel can never read data you couldn't already see, and can never change anything.
* Panels on a shared dashboard run **in each viewer's browser with that viewer's access**. Treat panel code like any other dashboard content: review a panel's code before relying on it.

## Panels on the dashboard

* Panels load **when scrolled into view**, with progress shown while the runtime loads, packages install and the script runs.
* When you change the selected runs:
  * with **Re-run automatically when the selected runs change** enabled, the panel re-runs about half a second after you stop clicking;
  * otherwise it keeps showing its last result and displays a **Runs changed — Refresh** button.
* **Fullscreen** keeps the panel running, so it doesn't restart. Press <kbd>Esc</kbd> or the button again to exit.
* Panels follow the app's **light/dark theme**. Switching theme restarts each panel, which takes about 10 seconds.

## Limitations

* **Data calls must be awaited.** Wrapping them in an ordinary synchronous function won't work; use top-level `await` or an `async def`.
* **matplotlib and seaborn figures are static images** (`st.pyplot`). For interactive charts use `st.plotly_chart`, `st.line_chart` or Altair.
* **Startup time:** the first render takes 10–30 seconds; later reruns take about 2 seconds. The browser caches the runtime after the first panel you load.
* **Memory:** each panel is its own Python runtime of about 100–200 MB, so keep the number of panels on one dashboard modest.
* **Fixed package set:** only the packages listed above.
* **Size:** code is limited to 64 KB per panel.
* **AI assistants read panels as code.** The [MCP dashboard tools](/pluto/mcp#dashboards) return a panel's source rather than an image of its output, because rendering it would mean running your Python on the server.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.