# pyobservablejs
> Reactive Observable notebooks from Python
This file contains all documentation content in a single document following the llmstxt.org standard.
## Examples
Each example pairs runnable Python with a live Observable notebook. Start with
the data path, input ownership, view layout, or file workflow closest to the
notebook you are building.
---
## Load an existing notebook with local files
A Notebook Kit document on disk often references data files next to it. This
example reads the document with Python, passes its text to `Notebook.from_html`,
and embeds the referenced CSV so the widget carries its data wherever it
renders.
The example writes the document and its data file to a temporary directory
first, standing in for a notebook you already have.
```python
from pathlib import Path
import tempfile
import observablejs as obs
base = Path(tempfile.mkdtemp(prefix="observable-source-"))
(base / "penguins.csv").write_text(
"species,count\nAdelie,152\nChinstrap,68\nGentoo,124\n",
encoding="utf-8",
)
source = """
"""
source_path = base / "penguins.html"
source_path.write_text(source, encoding="utf-8")
notebook = obs.Notebook.from_html(
source_path.read_text(encoding="utf-8"),
base_path=source_path.parent,
embed_file_attachments=True,
)
full_view = notebook.view()
full_view
```
The output renders species counts from the local CSV. The counts come from
the [Palmer Penguins
dataset](https://allisonhorst.github.io/palmerpenguins/).
## How the notebook carries the file
- `Path.read_text` loads the document text before `from_html` constructs the
source-backed notebook. The `glacier` theme and both cells come from that
string.
- `embed_file_attachments=True` discovers the literal
`FileAttachment("penguins.csv")` call, reads the file relative to the
explicit `base_path`, and registers it as a data URL in
`notebook.attachments`.
- Attachment discovery keeps the input document text unchanged. The
attachment registry carries the embedded bytes.
## Add modules or keep files remote
- Add `rewrite_imports=True` when the document imports local JavaScript
modules with relative specifiers. They are inlined recursively. See Add
files and local modules.
- Leave embedding disabled when the page already serves the files at the same
browser-relative paths.
- Override notebook values from Python by passing `variables=` to the
constructor, exactly as with authored notebooks.
---
## Coordinate Python and browser inputs
Interactive documents often need both kinds of control: host inputs that
live in Python, and Observable inputs that live in the browser. This example
drives one chart from each side and reads the browser-owned value back into
Python.
The marimo slider sets the _floor_ through a Python variable. The Observable
input sets the _gain_ entirely in the browser. The readout at the bottom is
a Python cell.
```python
import marimo as mo
import observablejs as obs
floor_slider = mo.ui.slider(
start=0,
stop=40,
step=5,
value=10,
label="Floor from Python",
)
```
```python
letters = [
{"letter": "A", "frequency": 8.2},
{"letter": "B", "frequency": 1.5},
{"letter": "C", "frequency": 2.8},
{"letter": "D", "frequency": 4.3},
{"letter": "E", "frequency": 12.7},
]
notebook = obs.Notebook(
obs.js(
"""
const gain = view(Inputs.range(
[1, 8],
{label: "Gain in the browser", step: 1, value: 3}
));
""",
key="gain_control",
),
obs.js(
"""
Plot.barY(letters, {
x: "letter",
y: (d) => Math.max(d.frequency * gain, floor),
fill: "letter",
tip: {
// This example references a theme variable used by mdx-marimo.
// Replace it with a CSS color or remove `fill` in another host.
fill: "var(--marimo-island-background)"
}
}).plot({
height: 260,
y: {grid: true, label: "Scaled frequency"},
color: {legend: false}
})
""",
key="chart",
),
variables={"letters": letters, "floor": 10},
)
full_view = notebook.view()
```
```python
notebook.update_variables({"floor": floor_slider.value})
mo.vstack([floor_slider, full_view])
```
```python
full_view.value # subscribe this cell to view synchronization
state = full_view.state
if (
not state.pending
and state.input_revision is not None
and state.settled_revision == state.input_revision
):
gain = state.result("gain_control")
gain_report = (
f"Python reads `gain = {gain.values['gain']}` "
f"while Python sets `floor = {notebook.variables['floor']}`."
if gain.status == "success"
else f"Browser error: {gain.errors}"
)
else:
gain_report = "Waiting for the Observable view to render."
mo.md(gain_report)
```
## Where each value comes from
| Value | Set by | Path |
| --------- | ------- | ------------------------------------------------------------------ |
| `letters` | Python | `variables=` when the notebook is created |
| `floor` | Python | `update_variables` after each marimo slider change |
| `gain` | Browser | `view(Inputs.range(...))`, then the finished `gain_control` result |
Two rules keep this pattern predictable:
- **Choose where each name is set.** Python sets `floor`. The browser sets
`gain`. See Share inputs across Python and
Observable when
both sides need to change one name.
- **Subscribe the reading cell.** The readout references `full_view.value`, so
marimo reruns it whenever the view synchronizes. Keep that reference in
the reading cell to receive every synchronized update.
See Send Python values and Read browser
results for each direction in isolation.
---
## Python data to an interactive chart
A Python list of dictionaries becomes a JavaScript array of objects. This
example passes per-species statistics from the [Palmer Penguins
dataset](https://allisonhorst.github.io/palmerpenguins/) to the browser,
where an Observable input chooses the plotted metric. The switch recomputes
entirely in the browser. Python is not involved after the initial render.
```python
import observablejs as obs
species_stats = [
{"species": "Adelie", "count": 152, "mean_mass_g": 3701},
{"species": "Chinstrap", "count": 68, "mean_mass_g": 3733},
{"species": "Gentoo", "count": 124, "mean_mass_g": 5076},
]
notebook = obs.Notebook(
obs.js(
"""
const metric = view(Inputs.select(
new Map([
["Penguin count", "count"],
["Mean body mass (g)", "mean_mass_g"]
]),
{label: "Metric"}
));
""",
key="metric_control",
),
obs.js(
"""
Plot.barX(speciesStats, {
x: metric,
y: "species",
fill: "species",
tip: {
// This example references a theme variable used by mdx-marimo.
// Replace it with a CSS color or remove `fill` in another host.
fill: "var(--marimo-island-background)"
}
}).plot({
height: 240,
marginLeft: 76,
color: {legend: true},
x: {grid: true, label: metric === "count" ? "Penguins" : "Body mass (g)"},
y: {label: null}
})
""",
key="chart",
),
variables={"speciesStats": species_stats},
)
full_view = notebook.view()
full_view
```
## Data crosses once
- The `variables` mapping publishes the Python list under the JavaScript
name `speciesStats`. The chart cell reads it like any graph variable.
- `view(Inputs.select(...))` defines `metric` in the browser. Changing it
invalidates only the chart cell.
- A pandas or Polars `DataFrame` works in place of the literal list. It
arrives as the same array of row objects. See Variables and
serialization.
## Update or scale the data path
- Update the records after render with
`notebook.update_variables({"speciesStats": ...})`. The chart recomputes while
the input keeps its selection. See Python to
Observable.
- For large tables, register a file with
`files=` and load it in the browser
with `FileAttachment(...).csv()`.
---
## Split one notebook into several views
One notebook definition can power several placements. This example defines a
hidden filter, a chart, and a summary in a single `Notebook`, then mounts the
chart and the summary as separate focused views in a two-column layout.
The views stay connected through the notebook session: one Python slider
updates the `minMass` variable, and both runtimes recompute.
```python
import marimo as mo
import observablejs as obs
mass_slider = mo.ui.slider(
start=2700,
stop=6300,
step=100,
value=4000,
label="Minimum body mass (g)",
)
```
```python
notebook = obs.Notebook(
obs.js(
"const heavyPenguins = penguins.filter((d) => d.body_mass_g >= minMass);",
key="heavy_penguins",
display=False,
),
obs.js(
"""
Plot.dot(heavyPenguins, {
x: "culmen_length_mm",
y: "body_mass_g",
fill: "species",
r: 4,
tip: {
// This example references a theme variable used by mdx-marimo.
// Replace it with a CSS color or remove `fill` in another host.
fill: "var(--marimo-island-background)"
}
}).plot({
height: 260,
color: {legend: true},
x: {grid: true, label: "Bill length (mm)"},
y: {grid: true, label: "Body mass (g)"}
})
""",
key="chart",
),
obs.js(
"""
html`
${heavyPenguins.length} of ${penguins.length}
penguins are at or above ${minMass} g.
`
""",
key="summary",
),
variables={"minMass": 4000},
)
chart_view = notebook.view("chart")
summary_view = notebook.view("summary")
```
```python
notebook.update_variables({"minMass": mass_slider.value})
mo.vstack(
[
mass_slider,
mo.hstack([chart_view, summary_view], widths=[2, 1]),
]
)
```
## How the views stay connected
- **Focused views carry their dependencies.** Each view selects one cell.
The hidden `heavy_penguins` cell evaluates inside both runtimes because
the selected cells depend on it.
- **One session update reaches every view.** The final cell calls
`update_variables` once. Both views receive the new `minMass` and run their
cells again.
- **Each view has its own state.** `chart_view.state` and `summary_view.state`
update independently.
## Sharing browser inputs across views
Named `viewof` inputs also share values across views of the same notebook:
when a user interacts with an input in one view, sibling views that render
an input with the same name receive the value in interactive hosts such as
marimo and JupyterLab. The contract, including which value shapes are
shared, is described in Bidirectional
inputs.
## Choose a composite view
If the chart and summary belong in _one_ placement, select them into one
view. `notebook.view("chart", "summary")` evaluates everything once
in a single runtime. Separate views fit separate placements such as different
cells of the host notebook, different layout columns, or different dashboard
pages. See Select cells and create
subsets.
---
## Use pyobservablejs with agents
`pyobservablejs` is independent of any notebook host. It renders through
anywidget in JupyterLab, marimo, VS Code notebooks, Google Colab, and other
compatible environments.
Each installation carries an Agent Plugin with a version-matched Agent Skill.
The skill teaches agents to construct `Notebook` controllers, render keyed
views, synchronize values, read browser state, attach files, and load notebook
sources through the public `observablejs` API.
## Read the packaged skill
Any Python process with `pyobservablejs` installed can locate the skill:
```python
import observablejs.agent as observablejs_agent
plugin = observablejs_agent.agent_plugin()
skill = observablejs_agent.agent_skill()
print(plugin.tree())
print(skill.body)
print(skill / "references" / "workflows.md")
```
These paths come from the installed distribution. The instructions and Python
API therefore share one package version.
## Add marimo as an optional agent host
The marimo integration adds discovery inside a live notebook. Four pieces take
part, and each has one job:
| Piece | Job |
| ---------------- | ------------------------------------------------------------ |
| `pyobservablejs` | Builds Observable notebooks and supplies its Agent Skill. |
| marimo | Hosts the Python notebook and its live kernel. |
| code mode | Gives an agent a Python API for the live notebook. |
| marimo pair | Connects a coding agent to marimo and teaches code-mode use. |
### 1. What is marimo?
[marimo](https://docs.marimo.io/) is an open-source reactive Python notebook.
It stores each notebook as a Python file. When a cell changes, marimo runs the
dependent cells or marks them stale. This keeps code, outputs, and in-memory
values consistent.
For `pyobservablejs`, marimo is one anywidget host. A `Notebook` and its views
keep the same public API in every supported host.
### 2. What is a code-mode agent?
A coding agent usually reads files, edits source, and runs commands. A
code-mode agent can also execute Python in a live marimo kernel. It can inspect
current cells and variables, apply validated cell edits, run cells, and read
their results.
Marimo exposes those notebook operations through `marimo._code_mode`. The API
is agent-facing and evolves with marimo. Agents call `help(cm)` to read its
current contract at runtime. The marimo engineering guide explains the
[code-mode transaction and validation model](https://marimo.io/blog/notebooks-as-a-tool-for-agents).
### 3. What is marimo pair?
[marimo pair](https://marimo.io/pair) connects a coding agent to a running
marimo notebook. It combines an Agent Skill with a bridge into the live kernel.
The agent can inspect notebook memory, use a temporary scratchpad, and commit
finished work as notebook cells.
`marimo pair` owns the connection and notebook-editing workflow. The packaged
`pyobservablejs` skill teaches the connected agent how to build and operate
Observable views.
## Discover pyobservablejs from marimo
Marimo code mode reads the `marimo.agent.capability` entry point without
importing provider modules. Capability discovery requires marimo 0.24.0 or
newer. Install or update the optional notebook host in the current environment:
```sh
uv add "marimo>=0.24.0"
```
Inspect its capability map and built-in guidance:
```python
import marimo._code_mode as cm
print(cm.capabilities()["pyobservablejs"])
help(cm)
```
The `pyobservablejs` entry resolves to `observablejs.agent`. Import that module
and read its generated help:
```python
import observablejs.agent as observablejs_agent
help(observablejs_agent)
```
## Read the documentation as Markdown
The documentation build publishes two entry points for language models:
- [`llms.txt`](https://peter-gy.github.io/pyobservablejs/llms.txt) maps the
guide, examples, and API reference to their documentation pages.
- [`llms-full.txt`](https://peter-gy.github.io/pyobservablejs/llms-full.txt)
combines the published documentation in one Markdown document.
Start with `llms.txt` when the agent can fetch pages on demand. Use
`llms-full.txt` when one complete context document fits the task.
---
## Choose a library
# Choose pyobservablejs or pyobsplot
Both libraries bring Observable tools into Python notebooks. Choose by the unit
you want to create.
| Need | Library | Result |
| -------------------------------------------------------------------------------------- | ------------------------------------------------ | -------------------------------------------------- |
| A reactive notebook with JavaScript, Markdown, HTML, inputs, files, and selected views | `pyobservablejs` | A Notebook Kit notebook rendered through anywidget |
| An Observable Plot figure described with a Python Plot specification | [`pyobsplot`](https://github.com/juba/pyobsplot) | A widget or static figure |
Use `pyobservablejs` when cells depend on one another or when Python and browser
inputs need to share values. Use `pyobsplot` when the Plot figure is the complete
output.
---
## Read browser results
`NotebookView.state` reports browser work as one consistent, read-only snapshot.
```python
import observablejs as obs
notebook = obs.Notebook(
obs.ojs(
'viewof gain = Inputs.range([0, 12], {value: 5, step: 1, label: "Gain"})',
key="gain_control",
),
obs.js("const doubled = gain * 2;", key="doubled", display=False),
obs.js(
'html`Gain is ${gain}. Doubled is ${doubled}.
`',
key="readout",
),
)
view = notebook.view()
view
```
Read a result after the latest input revision settles. In marimo, reference
`view.value` first so the reading cell reruns when the view synchronizes:
```python
view.value # rerun this cell when synchronized widget state changes
state = view.state
if (
not state.pending
and state.input_revision is not None
and state.settled_revision == state.input_revision
):
doubled = state.result("doubled")
if doubled.status == "success":
print(doubled.values["doubled"])
else:
print(doubled.errors)
```
Each `CellResult` has a `status`, `revision`, read-only `values`, and structured
`errors`. A failed output reports its error phase and optional variable name.
A multi-output cell may contain successful values beside errors.
## React to state changes
Traitlets-aware environments can observe the public state trait:
```python
def on_state(change):
state = change["new"]
if not state.pending:
print(state.results)
view.observe(on_state, names="state")
```
In marimo, reference `view.value` in the reading cell so that the cell reruns
when synchronized widget state changes, then read `view.state`.
State belongs to the view whose runtime produced it. A focused view reports
its selected result while hidden dependencies still evaluate:
```python
gain_view = notebook.view("gain_control")
```
See View state and graph for revision,
status, error, and value conversion contracts.
---
## Inspect dependencies
Each displayed view reports the dependencies for its selected cells and the
cells they need. Read them from `view.state.graph`.
```python
import observablejs as obs
notebook = obs.Notebook(
obs.js(
"""
const cylinders = view(Inputs.select(
["All", ...new Set(cars.map((d) => d.cylinders))],
{label: "Cylinders", value: "All"}
));
""",
key="cylinder_control",
),
obs.js(
"""
const filteredCars = cylinders === "All"
? cars
: cars.filter((d) => d.cylinders === cylinders);
""",
key="filtered_cars",
display=False,
),
obs.js(
"""
Plot.dot(filteredCars, {
x: "weight (lb)",
y: "economy (mpg)",
fill: "cylinders",
tip: {
// This example references a theme variable used by mdx-marimo.
// Replace it with a CSS color or remove `fill` in another host.
fill: "var(--marimo-island-background)"
}
}).plot({height: 300, x: {grid: true}, y: {grid: true}})
""",
key="chart",
),
)
view = notebook.view()
view
```
After the graph arrives:
```python
graph = view.state.graph
if graph is not None:
chart = graph.cell("chart")
print(chart.references)
for edge in graph.edges:
print(edge.source.key, "->", edge.target.key, "via", edge.variable)
```
`graph.cell(key)` uses public cell identity. Anonymous cells remain in
`graph.cells`. `cell_for_variable(name)` finds one unique defining cell.
`external_references` lists names supplied outside evaluated cells, including
referenced builtins and Python variables.
Export the same snapshot as Mermaid or D2:
```python
if graph is not None:
mermaid_source = graph.to_mermaid()
d2_source = graph.to_d2()
```
Diagram labels prefer cell keys and fall back to notebook position. See View
state and graph for record fields.
---
## Send Python values
`variables` sends named Python values to the browser. `update_variables`
changes them while views stay open. Cells that use a changed value run again.
Use separate marimo cells for the slider, notebook, and update.
```python
import marimo as mo
import observablejs as obs
minimum_mass = mo.ui.slider(
start=3000,
stop=6000,
step=250,
value=4000,
label="Minimum body mass (g)",
)
```
```python
notebook = obs.Notebook(
obs.js(
"""
const selectedPenguins = penguins.filter(
(d) => d.body_mass_g >= minimumMass
);
""",
key="selected_penguins",
display=False,
),
obs.js(
"""
Plot.barX(
selectedPenguins,
Plot.groupY(
{x: "count"},
{
y: "species",
fill: "species",
tip: {
// This example references a theme variable used by mdx-marimo.
// Replace it with a CSS color or remove `fill` in another host.
fill: "var(--marimo-island-background)"
}
}
)
).plot({
height: 240,
marginLeft: 76,
x: {grid: true, label: "Penguins at or above the threshold"},
y: {label: null}
})
""",
key="chart",
),
variables={"minimumMass": 4000},
)
view = notebook.view()
```
```python
notebook.update_variables({"minimumMass": minimum_mass.value})
mo.vstack([minimum_mass, view])
```
## Update, replace, or release
Both write methods accept exactly one mapping:
```python
notebook.update_variables({"minimumMass": 3500})
notebook.replace_variables({"minimumMass": 4000, "rows": rows})
notebook.reset_variables("minimumMass")
```
Update changes the listed names and keeps the rest. Replace uses the new mapping
as the complete set of Python values. Reset removes selected Python values so
the notebook can define them again. A real change reaches every active view.
An unchanged value is a no-op unless the browser has changed a shared input with
that name. In that case, the update clears the browser-owned value and reapplies
the Python value.
`Notebook.variables` is a separate read-only snapshot. Frameworks that listen
to traitlets can observe `Notebook.state` for changes.
See Variables and
serialization for supported
values and Read browser results for the return
path.
---
## Share inputs across Python and Observable
One named input can receive updates from Python and the browser. The Python
slider writes through `update_variables`. The Observable slider changes the
browser value, and a Python readout follows the finished result.
Create and display the Python control in its own marimo cell.
```python
import marimo as mo
import observablejs as obs
python_gain = mo.ui.slider(
start=0,
stop=12,
value=5,
label="Set gain from Python",
)
python_gain
```
Create and display the browser view in another cell.
```python
notebook = obs.Notebook(
obs.ojs(
'viewof gain = Inputs.range([0, 12], {value: 5, step: 1, label: "Gain in the browser"})',
key="gain_control",
),
obs.js(
"""
Plot.barY(
Array.from({length: 8}, (_, index) => (index + 1) * gain),
{
x: (_, index) => index + 1,
y: (value) => value,
tip: {
// This example references a theme variable used by mdx-marimo.
// Replace it with a CSS color or remove `fill` in another host.
fill: "var(--marimo-island-background)"
}
}
).plot({height: 220, x: {label: "Step"}, y: {grid: true}})
""",
key="chart",
),
variables={"gain": 5},
)
view = notebook.view()
view
```
Send Python slider changes from a cell that references `python_gain` and
`notebook`.
```python
notebook.update_variables({"gain": python_gain.value})
```
Read the current browser value after the latest input revision settles.
```python
view.value # subscribe this cell to view synchronization
state = view.state
if (
not state.pending
and state.input_revision is not None
and state.settled_revision == state.input_revision
):
gain = state.result("gain_control")
gain_report = (
f"Python reads `gain = {gain.values['gain']}`."
if gain.status == "success"
else f"Browser error: {gain.errors}"
)
else:
gain_report = "Waiting for the Observable view to render."
mo.md(gain_report)
```
Each direction has one path:
1. The Python slider calls `update_variables`.
2. The Observable slider updates the named `viewof gain` input.
3. The synchronized anywidget state updates `view.state`.
4. The readout displays the finished `gain_control` result.
The Python slider shows the last value Python sent. The Observable slider and
readout show the current browser value.
Shared input state supports JSON-like values, `BigInt`, dates, maps, and sets.
Functions, DOM elements, files, and binary buffers stay in the view where they
were created. Use the same input shape for a shared name across views.
See Compose views for separate views
and Variables and
serialization
for the value boundary.
---
## Author notebook cells
`Notebook` accepts cells from four helpers. Each helper wraps a source string
in one Notebook Kit cell mode.
| Helper | Cell source |
| --------------- | -------------------------------- |
| `obs.js(...)` | Standard Notebook Kit JavaScript |
| `obs.ojs(...)` | Observable JavaScript |
| `obs.md(...)` | Markdown |
| `obs.html(...)` | HTML |
Top-level declarations in JavaScript cells form a reactive graph: when a
value changes, every cell that references it runs again. Move the exponent
control to recompute the readout in the browser.
```python
import observablejs as obs
notebook = obs.Notebook(
obs.js(
"""
const exponent = view(Inputs.range(
[1, 8],
{label: "Exponent", step: 1, value: 2}
));
""",
key="exponent_control",
),
obs.js(
'html`Two to the power of ${exponent} is ${2 ** exponent}.
`',
key="readout",
),
)
full_view = notebook.view()
full_view
```
The first cell defines `exponent`. The readout references it, so Notebook Kit
runs that cell again when the input changes. Notebook Kit schedules cells
from their dependencies, independent of source order.
## JavaScript cells
An expression cell displays its value implicitly.
```python
obs.js("Plot.lineY(aapl, {x: 'Date', y: 'Close'}).plot()")
```
A program cell contains declarations or statements. Call `display(...)` when
a program cell should render a value.
```python
obs.js(
"""
const formatter = new Intl.NumberFormat("en-US");
display(formatter.format(42000));
"""
)
```
`view(...)` displays an input and defines a reactive value from its events.
```python
obs.js(
'const radius = view(Inputs.range([2, 12], {value: 5, label: "Radius"}));'
)
```
## Observable JavaScript cells
`obs.ojs` creates a classic Observable JavaScript cell. Use it for existing
Observable JavaScript source, `viewof` declarations, and imported Observable
notebook code.
```python
obs.ojs(
'viewof radius = Inputs.range([2, 12], {value: 5, label: "Radius"})'
)
```
Both cell modes participate in the same Notebook Kit graph.
## Markdown and HTML cells
`obs.md` renders Markdown. `obs.html` renders HTML.
```python
notebook = obs.Notebook(
obs.md("## Filtered records"),
obs.html("Status: loaded
"),
)
```
## Cell options
Every helper accepts the same options.
```python
obs.js(
"const filtered = [1, 2, 3].filter((value) => value >= 2);",
key="filtered_rows",
display=False,
pinned=True,
)
```
- `key` is the public identity used by `notebook.cell("filtered_rows")`,
`notebook.view("filtered_rows")`, readback, and graph lookup. It survives
Notebook Kit HTML round trips.
- `display=False` hides the cell output while keeping its values in the
graph.
- `pinned=True` exposes the source when the cell is selected and the notebook
enables `show_pinned_source`. See Themes and
source.
- `raw=True` preserves the source string exactly, including indentation and
surrounding newlines.
- `id` is Notebook Kit serialization metadata. `output` and
`notebookkit_attrs` expose mode-specific Notebook Kit metadata.
See Cell helpers for the full signature and
error behavior, and Browser execution
for the builtins available to each cell.
---
## Add files and local modules
`files` registers named inputs for Observable's `FileAttachment` builtin.
Local files are read during `Notebook` construction and sent to the browser
as data URLs, so the widget carries its data wherever it renders.
```python
from pathlib import Path
import tempfile
import observablejs as obs
data_dir = Path(tempfile.mkdtemp(prefix="observable-files-"))
(data_dir / "counts.csv").write_text(
"species,count\nAdelie,152\nChinstrap,68\nGentoo,124\n",
encoding="utf-8",
)
notebook = obs.Notebook(
obs.js(
'const speciesCounts = FileAttachment("counts.csv").csv({typed: true});',
key="species_counts",
display=False,
),
obs.js(
"""
Plot.barY(speciesCounts, {
x: "species",
y: "count",
fill: "species",
tip: {
// This example references a theme variable used by mdx-marimo.
// Replace it with a CSS color or remove `fill` in another host.
fill: "var(--marimo-island-background)"
}
}).plot({
height: 260,
color: {legend: true},
y: {grid: true, label: "Penguins"}
})
""",
key="chart",
),
files={"counts.csv": "counts.csv"},
base_path=data_dir,
)
full_view = notebook.view()
full_view
```
The mapping key is the name used by `FileAttachment`. `typed: true` asks the
CSV reader to convert values such as numbers and dates. The counts come from
the [Palmer Penguins
dataset](https://allisonhorst.github.io/palmerpenguins/).
## Accepted `files` values
Each value can be a local path, a URL, or a prepared attachment record.
```python
notebook = obs.Notebook(
obs.ojs('countries = FileAttachment("countries.json").json()'),
files={
"countries.json": {
"url": "https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json",
"mimeType": "application/json",
}
},
)
```
Relative local paths resolve against `base_path`, which defaults to the
current working directory. See File
attachments for path resolution, record
shapes, and browser access methods.
## Discover files in source HTML
`Notebook.from_html` can discover local files referenced by literal
`FileAttachment("name")` calls and embed them as data URLs:
```python
from pathlib import Path
import observablejs as obs
source_path = Path("notebooks/report.html")
notebook = obs.Notebook.from_html(
source_path.read_text(encoding="utf-8"),
base_path=source_path.parent,
embed_file_attachments=True,
)
```
- `embed_file_attachments=True` discovers local `FileAttachment("...")`
references in notebook script cells and embeds each file as a data URL.
- `rewrite_imports=True` inlines local modules referenced by quoted relative
specifiers in static imports, `export ... from` declarations, and dynamic
`import(...)` calls, following local imports recursively. Computed
specifiers and paths that do not resolve to files stay unchanged.
Both options need a `base_path` because the source string has no filesystem
location. Choose the directory that should resolve the document's relative
attachment and module paths.
Leave both options false when the page already serves the referenced files at
the same relative paths. When discovery is enabled, an explicit `files` entry
takes precedence over a discovered file with the same name.
Use URL-backed attachments for large data that the browser should load outside
widget state. Local files are embedded as data URLs and increase the
synchronized payload.
---
## Load Notebook Kit HTML
`Notebook.from_html` creates a source-backed notebook from a Notebook Kit HTML
string. Define the string in Python, read it from a file, fetch it from
storage, or receive it from another API before construction. The example uses
Notebook Kit's built-in AAPL sample.
Imported cells run with the host page's browser privileges. Review the trust
boundary in Browser execution.
```python
import observablejs as obs
source = """
"""
notebook = obs.Notebook.from_html(source)
full_view = notebook.view()
full_view
```
The widget preserves the `glacier` theme and renders the built-in AAPL sample
as an area chart.
## Source-backed notebooks
A notebook loaded from HTML keeps that Notebook Kit document as its
definition. Its theme comes from the document. Assigning `notebook.theme` raises
`traitlets.TraitError`.
Everything else works the same as a Python-authored notebook: `view()`
creates renderable views, `variables` overrides named values, and readback
synchronizes after render.
```python
notebook = obs.Notebook.from_html(source, variables={"threshold": 0.5})
```
## Load from a file
Read the document with Python, then pass its text to `Notebook.from_html`:
```python
from pathlib import Path
import observablejs as obs
path = Path("notebooks/report.html")
source = path.read_text(encoding="utf-8")
notebook = obs.Notebook.from_html(source)
```
When the document references local files through `FileAttachment` or imports
local JavaScript modules, pass the directory that should resolve those paths:
```python
notebook = obs.Notebook.from_html(
source,
base_path=path.parent,
embed_file_attachments=True,
rewrite_imports=True,
)
```
See Add files and local modules for how discovery
and embedding work, and the Load an existing notebook with local
files example for a complete notebook.
## Round trip
`notebook.to_notebook_html()` returns the Notebook Kit document for any
notebook, so HTML can round-trip through Python. See Export Notebook Kit
HTML.
Source constructors defines the full
contract, including accepted source shapes and error behavior.
---
## Import ObservableHQ notebooks
`Notebook.from_observablehq` fetches a public ObservableHQ notebook through
the document API and returns a `Notebook` definition. Call `view()` to create
its renderable view. Imported cells run with the classic Observable standard
library, including `require`, `html`, `Generators`, `Mutable`, and `DOM`.
Imported notebooks and remote modules run with the host page's browser
privileges. Review the trust boundary in Browser
execution.
```python
import observablejs as obs
notebook = obs.Notebook.from_observablehq("@observablehq/plot-scatterplot/2")
full_view = notebook.view()
full_view
```
The `specifier` can be an ObservableHQ URL, a notebook slug, a notebook id,
or a document API URL. Use `timeout` to bound the fetch.
```python
obs.Notebook.from_observablehq("https://observablehq.com/@d3/bar-chart")
obs.Notebook.from_observablehq("@d3/bar-chart", timeout=10)
```
The constructor fetches the document on every call. To skip that request
during a build or test, save the response once and pass it to
`Notebook.from_observablehq_document(...)`.
Imports, uploaded files, libraries, and datasets may still load from the
network when the view renders.
## Keep imported notebooks pinned
The fetched document identifies its source revision. `pyobservablejs` uses
that revision when resolving imported Observable notebooks, so their
dependency versions match the source notebook.
Keep `id` and `version` when saving a document API response. Passing the
saved mapping to `from_observablehq_document` restores the same import
resolution. For a node collection, pass a document mapping such as
`{"nodes": nodes}`. Add the source `id` and `version` when imports must stay
pinned to that published revision.
## Override variables
Pass `variables` to override notebook-defined values from Python.
```python
document = {
"id": "1234567890abcdef",
"version": 7,
"title": "Report",
"nodes": [
{"id": 1, "mode": "js", "name": "answer", "value": "answer = 42"},
{"id": 2, "mode": "js", "value": "md`Answer: ${answer}`"},
],
}
notebook = obs.Notebook.from_observablehq_document(
document,
variables={"answer": 100},
)
```
Remote uploaded files become URL-backed file records. Explicit `files`
override fetched files with the same name.
## Runtime profile
Every ObservableHQ constructor selects the _observable_ runtime profile,
which supplies the classic Observable standard library and `require`.
Python-authored notebooks select the Notebook Kit builtins. The profile is
fixed for the notebook session and survives
`to_notebook_html()` round trips. See Browser execution and network
access.
See Source constructors for the
constructor contracts, error behavior, and source-revision import resolution.
---
## Browser execution and network access
:::warning Trusted input
Notebook cells run as JavaScript in the page that hosts the widget. Treat
imported Notebook Kit HTML, ObservableHQ notebooks, and their remote modules as
trusted code because they can use the page's browser privileges.
:::
All JavaScript evaluates in the browser page that hosts the view. Python
never executes JavaScript, and cells never execute Python. This page
describes what the browser runtime provides and when it reaches the network.
## Runtime profiles
The notebook source selects the standard-library profile before a view
starts. The profile is fixed for the notebook session and inherited by each
view.
| Source | Profile |
| ------------------------------------------------------------ | ------------ |
| Python-authored cells | Notebook Kit |
| Notebook Kit HTML | Notebook Kit |
| `Notebook.from_observablehq*` | Observable |
| ObservableHQ-derived HTML serialized by `to_notebook_html()` | Observable |
The Notebook Kit profile uses the builtins exported by Notebook Kit. The
Observable profile uses the classic Observable standard library, which adds
classic names such as `require` and `DOM`. Both profiles receive view-scoped
`FileAttachment`, `document`, `width`, and `dark` values from
`pyobservablejs`.
## Builtins
The selected profile resolves builtins when cells reference them.
| Builtin | Contract |
| ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Inputs` | Creates controls, tables, and other input elements. Pass an input to `view(...)` to expose its changing value. |
| `Plot` | Creates Observable Plot marks. `Plot.plot()` and a mark's `.plot()` return DOM nodes. |
| `html`, `md` | Creates reactive HTML and Markdown DOM values from tagged templates. |
| `Generators` | Provides `input`, `observe`, and `queue` in both profiles. Other methods come from the selected standard library. |
| `Mutable` | Creates a reactive source with a `.value` getter and setter. Consumers rerun after the value changes. |
| `width` | Yields the floored notebook root width with a 320-pixel minimum. It falls back to 928 when layout has no measurable width and updates after resize. |
| `dark` | Yields whether the notebook root uses a dark color scheme. Theme changes rerun dependent cells. |
The Notebook Kit profile also exposes sample datasets such as `aapl`,
`cars`, and `penguins`. Runtime-owned builtin names cannot be Python
variables. The full reserved list is in Variables and
serialization.
## Module imports and the network
Standard JavaScript cells can import an npm package or browser module:
```python
obs.js(
"""
import {format} from "npm:d3-format@3";
const compact = format(".2s");
display(compact(42000));
"""
)
```
External libraries, sample datasets, module imports, and source notebooks
can require browser network access. Notebook Kit resolves `npm:` specifiers
from jsDelivr at render time. The page content security policy must permit
every package, data, and module origin used by its cells. To keep a widget
self-contained, embed local modules with `rewrite_imports=True` and local
data with `embed_file_attachments=True`. See Add files and local
modules.
Network access happens at two distinct times:
| When | What | Where it runs |
| ----------------------- | -------------------------------------------------------------------------------- | ------------- |
| `Notebook` construction | `from_observablehq` fetches the document API. Local files are read and embedded. | Python |
| View render | npm imports, URL-backed attachments, remote datasets, imported notebooks. | Browser |
## Promises, generators, and invalidation
Notebook Kit tracks asynchronous values as part of the graph.
| Value | Evaluation | Invalidation |
| ---------------------------- | ---------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| Promise | Dependent cells wait for the resolved value. | A stale resolution is ignored. The operation continues unless the cell cancels it. |
| Generator or async generator | The first yield defines the value. Later yields rerun dependent cells. | The runtime calls the generator's `return()` method. |
| `invalidation` | Provides a promise for the current cell evaluation. | Resolves when the evaluation is invalidated or its runtime is disposed. |
Use `invalidation` to release resources owned by a cell. See Display
views for the corresponding Python
lifecycle.
---
## Export Notebook Kit HTML
`to_notebook_html()` returns the current notebook definition as a Notebook
Kit document. Loaded documents preserve import rewrites requested during
construction.
```python
import observablejs as obs
notebook = obs.Notebook(
obs.md("# Report", key="title"),
obs.js(
"const total = rows.reduce((sum, row) => sum + row.value, 0);",
key="total",
display=False,
),
obs.js("html`Total: ${total}
`", key="summary"),
title="Weekly report",
theme="glacier",
variables={"rows": [{"value": 12}, {"value": 30}]},
)
print(notebook.to_notebook_html())
```
```text
Weekly report
```
The output is a standard [Notebook
Kit](https://observablehq.com/notebook-kit/) document: `title` becomes a
``, the theme becomes the `` attribute, `display=False`
becomes `hidden`, and each cell keeps its key and assigned id.
## Definition and session state
The document carries the notebook definition. Python keeps two kinds of state
in the session:
- **Python variables.** The exported HTML references `rows`, but the value
lives in the session. A consumer of the document must define `rows` itself
or load the HTML back with `from_html(..., variables=...)`.
- **File attachment records.** `FileAttachment("name")` calls stay in the
source, but the registered data URLs and URL records are session state.
When the text is consumed independently, its environment must provide the
referenced files.
Cell keys use the `data-pyobservablejs-key` script attribute. Loading the HTML
restores the same public identity. The script `id` remains Notebook Kit
serialization metadata.
## Round trips
Exported HTML loads back with full fidelity of the definition:
```python
html_text = notebook.to_notebook_html()
restored = obs.Notebook.from_html(html_text, variables={"rows": []})
```
ObservableHQ-derived notebooks record their classic standard-library profile
in the exported document, so a round trip through `from_html` preserves
`require` and the other classic builtins.
## Use with Notebook Kit tooling
The exported file is valid input for Notebook Kit's own tooling, so a
notebook authored in Python can graduate to a standalone Observable project:
```python
from pathlib import Path
Path("report.html").write_text(notebook.to_notebook_html(), encoding="utf-8")
```
See
`Notebook.to_notebook_html`
for the serialization contract and File
attachments for
the attachment boundary.
---
## Style notebooks and show source
`theme` accepts a Notebook Kit theme name or a light and dark mapping.
Pinned cells selected by the view appear in a source panel when
`show_pinned_source=True`.
```python
import observablejs as obs
notebook = obs.Notebook(
obs.md("## AAPL closing price"),
obs.js(
"""
Plot.lineY(aapl, {
x: "Date",
y: "Close",
tip: {
// This example references a theme variable used by mdx-marimo.
// Replace it with a CSS color or remove `fill` in another host.
fill: "var(--marimo-island-background)"
}
}).plot({
height: 260,
y: {grid: true, label: "Close ($)"}
})
""",
key="price_chart",
pinned=True,
),
theme={"light": "cotton", "dark": "slate"},
show_pinned_source=True,
)
full_view = notebook.view()
full_view
```
The rendered view switches between `cotton` and `slate` with the browser
color scheme preference. It also exposes the chart source in its source
panel.
## Theme names
Use a theme name exposed by `observablejs.NOTEBOOK_THEMES`:
```python
import observablejs as obs
obs.NOTEBOOK_THEMES
# ("air", "coffee", "cotton", "deep-space", "glacier", "ink", "midnight",
# "near-midnight", "ocean-floor", "parchment", "slate", "stark", "sun-faded")
```
Assigning `notebook.theme` on a Python-authored notebook updates its active
views live:
```python
notebook.theme = "glacier"
notebook.theme = {"light": "air", "dark": "near-midnight"}
```
Source-backed notebooks take their theme from the source HTML, where
Notebook Kit expresses the same mapping as an attribute:
```html
```
Changing `theme` on a source-backed notebook raises `traitlets.TraitError`
so the session cannot diverge from its document.
## Pinned source
`pinned=True` marks a cell whose source is worth showing. The panel appears
only when both sides opt in: the cell is pinned _and_ the notebook sets
`show_pinned_source=True`. Source-backed notebooks respect the `pinned`
attributes already present in the document.
See Themes for accepted names and validation
rules.
---
## Select cells
Call `Notebook.view(...)` to render all cells or a keyed selection.
| Selection | Call |
| ------------------- | ----------------------------------- |
| Whole notebook | `notebook.view()` |
| One keyed cell | `notebook.view("chart")` |
| Several keyed cells | `notebook.view("chart", "summary")` |
Cells needed by the selection also run, but their output stays hidden. Selected
cells render in notebook order, regardless of selector order.
```python
import observablejs as obs
numbers = obs.js(
"const numbers = [2, 4, 8, 16];",
key="numbers",
display=False,
)
chart = obs.js(
"""
Plot.barY(numbers, {
x: (_, index) => index + 1,
y: (value) => value * scale,
tip: {
// This example references a theme variable used by mdx-marimo.
// Replace it with a CSS color or remove `fill` in another host.
fill: "var(--marimo-island-background)"
}
}).plot({height: 220, x: {label: "Position"}, y: {grid: true}})
""",
key="chart",
)
summary = obs.js(
"""
html`
${numbers.length} values,
ending at ${numbers.at(-1) * scale}.
`
""",
key="summary",
)
notebook = obs.Notebook(numbers, chart, summary, variables={"scale": 2})
selected = notebook.view(summary, "chart")
selected
```
Although `summary` was passed first, the chart renders first because it appears
earlier in the notebook. `numbers` evaluates once as a hidden dependency.
## Selector contract
`view(*selectors)` accepts key strings, keyed authored `Cell` objects, and
`NotebookCell` handles from the same notebook.
```python
notebook.view("chart")
notebook.view(chart)
notebook.view(notebook.cell("chart"))
notebook.view(chart, notebook.cell("summary"))
```
Unknown keys raise `KeyError`. Each authored `Cell` selector needs a key, and
each `NotebookCell` selector must belong to the notebook. Duplicate selections
and foreign handles raise `ValueError`. Invalid selector types raise
`TypeError`.
`NotebookCell.key` is public identity. `NotebookCell.id` is Notebook Kit
serialization metadata, and `NotebookCell.index` is notebook-order metadata.
Anonymous cells remain available through `notebook.cells`.
## One composite view or separate views
A combined view evaluates selected outputs together and reports one state
snapshot. Separate views evaluate independently but receive the same notebook
values. Use a combined view for outputs that belong together and separate views
for different locations.
See Split one notebook into several
views for a complete layout.
---
## Compose views
Create several `NotebookView` objects from one `Notebook` when outputs belong
in different host locations. Values stay synchronized, while each view keeps
its own browser results.
```python
import marimo as mo
import observablejs as obs
control = obs.js(
"""
const threshold = view(Inputs.range(
[0, 1],
{label: "Threshold", step: 0.05, value: 0.5}
));
""",
key="threshold_control",
)
readout = obs.js(
'html`Threshold: ${threshold}
`',
key="threshold_readout",
)
notebook = obs.Notebook(control, readout)
control_view = notebook.view(control)
readout_view = notebook.view("threshold_readout")
mo.hstack(
[
control_view,
readout_view,
]
)
```
The readout needs `threshold`, so it also runs the control cell and hides that
cell's output. Moving the visible input shares the new value with the readout,
which then runs again.
## Shared notebook data, independent view state
```mermaid
flowchart TB
notebook["Notebook controller"] --> session["Shared session state"]
notebook --> control["Control NotebookView"]
notebook --> readout["Readout NotebookView"]
session --> runtimeA["Runtime A"]
session --> runtimeB["Runtime B"]
control --> runtimeA
readout --> runtimeB
runtimeA --> stateA["control_view.state"]
runtimeB --> stateB["readout_view.state"]
```
The notebook shares its cells, theme, files, Python variables, and browser input
values. Each view keeps its own selected cells, rendered output, state, and
lifecycle.
```python
notebook.update_variables({"threshold": 0.8})
control_state = control_view.state
readout_state = readout_view.state
```
The two views may finish at different times. Read or observe each state
separately.
A view can appear in one live location. Create another view for a second
location. Closing one view leaves the others active.
Contributor-facing model references, private traits, revision transport, and
teardown ordering live in the repository's [view composition development
guide](https://github.com/peter-gy/pyobservablejs/blob/main/development_docs/view-composition.md).
---
## Display a notebook
`notebook.view()` returns a renderable view backed by a `NotebookView`. Browser
evaluation starts when an anywidget host mounts it.
```python
import marimo as mo
import observablejs as obs
notebook = obs.Notebook(
obs.js(
"""
const threshold = view(Inputs.range(
[0, 1],
{label: "Threshold", step: 0.05, value: 0.5}
));
""",
key="threshold",
),
obs.js(
'html`Threshold: ${threshold}
`',
key="readout",
),
)
view = notebook.view()
view
```
## Display in marimo and Jupyter
In marimo, use the returned view as a cell output or layout child. In
JupyterLab, Jupyter Notebook, VS Code notebooks, and Google Colab, leave the
view as the final cell expression or pass it to `IPython.display.display`.
Colab requires the third-party widget support described by
[anywidget](https://anywidget.dev/en/notebooks/colab/).
## One view per placement
One `NotebookView` can appear in one live location at a time. Create a separate
view for each location.
```python
chart_a = notebook.view("readout")
chart_b = notebook.view("readout")
mo.hstack([chart_a, chart_b])
```
The views receive the same notebook values and supported browser inputs. Each
keeps its own rendered output, progress, results, errors, and graph.
## Skip Python state capture
Use `capture_state=False` for a screenshot, preview, or embed when Python never
reads `view.state`.
```python
preview = notebook.view(
capture_state=False,
)
preview
```
The view still evaluates, renders, responds to input, and receives Python
variable updates. Supported input controls continue to synchronize with other
views from the same notebook. `preview.state` stays at its initial value.
With capture disabled, result serialization and transport are skipped. The
avoided work grows with cell result size and update frequency, so small static
views usually see little difference. Keep the default `True` when Python reads
or observes `view.state`.
## Evaluation lifecycle
Before display, both revision fields are `None`. Display starts revision `0`
and sets `pending=True`. A revision finishes after every selected cell either
succeeds or fails.
```python
state = view.state
ready = (
not state.pending
and state.input_revision is not None
and state.settled_revision == state.input_revision
)
```
Python variable updates and browser inputs start a new revision when selected
cells need to run again. Assigning a new theme restarts each open view. Create a
new view for a different selection. Create a new notebook for a different cell
definition or set of files. A restarted view clears its previous graph while
work is pending.
## Close views
```python
view.close()
notebook.close()
```
`view.close()` stops one browser run and ignores any late results.
`notebook.close()` closes every view created from that notebook. Repeated calls
do nothing.
Cells that own browser resources should release them through Observable's
`invalidation` promise:
```python
obs.js(
"""
const controller = new AbortController();
invalidation.then(() => controller.abort());
const response = await fetch(dataUrl, {signal: controller.signal});
const records = await response.json();
"""
)
```
Continue with Select cells or Compose views.
---
## How pyobservablejs works
`Notebook` holds cells and values. `NotebookCell` points to one cell in that
notebook. `NotebookView` renders all or selected cells in the browser.
| Object | Contract |
| -------------- | ------------------------------------------------------------------------------------------ |
| `Notebook` | Holds cells or imported source, files, theme, Python variables, and shared browser inputs. |
| `NotebookCell` | Points to a cell by `key`. Its `index` and `id` are metadata. |
| `NotebookView` | Renders selected cells and reports one read-only `state` snapshot. |
Authored helpers such as `obs.js(..., key="chart")` return immutable `Cell`
objects. The `key` becomes the portable public identity after the cell enters a
notebook.
```python
import observablejs as obs
readout = obs.js(
'html`Doubled: ${doubled}
`',
key="readout",
)
notebook = obs.Notebook(
obs.js("const doubled = base * 2;", key="doubled", display=False),
readout,
variables={"base": 21},
)
full_view = notebook.view()
readout_view = notebook.view(readout)
same_view = notebook.view(notebook.cell("readout"))
```
## Python defines, the browser runs
Python sends the notebook and its values to the browser. Notebook Kit runs the
cells there. Each view sends structured results back to Python.
```mermaid
flowchart LR
notebook["Notebook"] -->|"cells and values"| shared["Shared notebook data"]
notebook -->|"view(selectors...)"| view["NotebookView"]
shared --> browser["Notebook Kit in the browser"]
view --> browser
browser --> state["NotebookView.state"]
```
`Notebook.state` is a separate, read-only snapshot of variables, files, and
theme. Frameworks that listen to traitlets can observe it:
```python
def on_state(change):
print(change["new"].variables)
notebook.observe(on_state, names="state")
notebook.update_variables({"base": 30})
```
`NotebookView.state` reports progress, cell results, errors, and the dependency
graph. Read results after `settled_revision` catches up with `input_revision`.
## One browser run per view
Views from one notebook receive the same Python variables and supported
`viewof` input values. Each keeps its own rendered output, progress, results,
errors, and graph. Select related outputs in one call when they should run
together:
```python
dashboard = notebook.view("doubled", "readout")
```
Selected cells render in notebook order. Cells they need also run, with their
output hidden.
Continue with Display a notebook, Select
cells, and Send Python values.
---
## Quickstart
`pyobservablejs` supports Python 3.11 through 3.14. Add it to an existing
environment:
```sh
uv pip install pyobservablejs
```
Or open an isolated marimo editor with the package available:
```sh
uvx --with pyobservablejs marimo edit notebook.py
```
## Create and render a notebook
Paste this into a marimo cell or another anywidget notebook host:
```python
import observablejs as obs
control = obs.ojs(
"""
viewof threshold = Inputs.range([0, 1], {
value: 0.5,
step: 0.1,
label: "Threshold"
})
""",
key="threshold",
)
result = obs.js(
'html`Threshold: ${threshold}`',
key="result",
)
notebook = obs.Notebook(
control,
result,
variables={"threshold": 0.5},
)
view = notebook.view()
view
```
## Update the browser from Python
`Notebook.update_variables` accepts one mapping. Every mounted view from this
notebook receives the update.
```python
notebook.update_variables({"threshold": 0.8})
```
## Read a settled result
`NotebookView.state` is one consistent, read-only snapshot. Read a result after
the current input revision finishes. In marimo, reference `view.value` first so
the cell reruns when the view synchronizes.
```python
view.value # rerun this cell when synchronized widget state changes
state = view.state
if (
not state.pending
and state.input_revision is not None
and state.settled_revision == state.input_revision
):
result_state = state.result("result")
if result_state.status == "success":
print(result_state.values)
else:
print(result_state.errors)
```
Continue with How pyobservablejs works, Author notebook
cells, Select cells, or Read browser
results.
---
## Troubleshooting
## State has no results yet
`NotebookView.state` starts empty before a browser mounts the widget. Inspect
the revisions and pending flag before consuming a result:
```python
state = full_view.state
if (
not state.pending
and state.input_revision is not None
and state.settled_revision == state.input_revision
):
result = state.result("chart")
```
Both revisions are `None` before mount. `pending=True` means a newer input
revision is still evaluating. Readback belongs to one view, so rendering a
sibling does not populate this state. See Read browser
results.
## A cell result has `status="error"`
Inspect `result.errors`. Every error includes a `name`, `message`, and phase.
Cell errors may also identify the failed output variable.
```python
result = full_view.state.result("chart")
for error in result.errors:
print(error.phase, error.variable, error.message)
```
The phase distinguishes source analysis, evaluation, rendering, and value
serialization. A multi-output cell can retain successful values beside an
error.
## The view displays nothing
- **marimo:** use the view as the cell output or pass it to a marimo layout.
- **Jupyter and most hosts:** the view must be the final expression of a
cell, or passed to `display(...)`.
- **Colab:** enable third-party widget support, per
[anywidget's Colab notes](https://anywidget.dev/en/notebooks/colab/).
- **Program cells:** a JavaScript cell with statements renders nothing
unless it calls `display(...)` or `view(...)`. Expression cells display
implicitly. See Author notebook
cells.
## The same view appears twice and one copy froze
A `NotebookView` can appear in one live location at a time. Displaying the same
view object twice leaves one copy frozen. Create one view per location. Views
from the same notebook stay synchronized. See Display a
NotebookView.
## `ValueError` for a variable name
Variable names must match `^[A-Za-z_$][0-9A-Za-z_$]*$`, and runtime-owned
builtin names such as `penguins`, `Plot`, or `width` are reserved. Rename
the Python variable. The reserved list is in Variables and
serialization.
## `TypeError: Value of type ... is not serializable`
The serialization
table lists
the types that can move between Python and the browser. Convert custom objects to plain mappings,
lists, or DataFrames first. Use file
attachments for large tables and binary data.
## A value is missing from a cell result
Read values from the result for the cell that defines them. Results stay
separate even when several cells define the same JavaScript variable name.
```python
value = full_view.state.result("summary").values["total"]
```
## The chart did not react to `update_variables`
- An unchanged value is a no-op unless the browser has changed a shared input
with that name. In that case, the update clears the browser-owned value and
reapplies the Python value. Mutating a list in place changes the browser value
only when its contents differ.
- The cell must reference the variable by that exact name. Check
`full_view.state.graph.external_references` to see what the cells actually
resolve from the environment. See Inspect the dependency
graph.
## A marimo cell shows stale readback
marimo reruns a cell when a UI element it references changes. Reference the
view's `value` in the reading cell:
```python
full_view
```
```python
full_view.value # subscribe
full_view.state
```
## `TraitError` when assigning `notebook.theme`
Source-backed notebooks take their theme from the source HTML. Edit the
`` attribute in the document. See Themes and
pinned source.
## `RuntimeError: Cannot mutate a closed Notebook`
`notebook.close()` closes the session and every view. `view.close()` closes
one view. After closing, variable mutators and `view()` raise `RuntimeError`.
Cell handles, attachments, variables, and theme stay readable. Recreate the
notebook to continue.
## `require is not defined` in an authored notebook
`require` belongs to the classic Observable standard library selected by
ObservableHQ imports. Python-authored and Notebook Kit HTML notebooks use
the Notebook Kit profile. Import npm packages with
`import ... from "npm:..."`. See Browser execution and network
access.
## Imports or datasets fail to load in a restricted environment
npm imports, URL-backed attachments, and ObservableHQ imports load from the
network in the browser at render time, and the page content security policy
must permit those origins. Embed local modules and files with
`rewrite_imports=True` and `embed_file_attachments=True` to reduce runtime
fetches. See Browser execution and network
access.
## `OSError` or `ValueError` from `from_observablehq`
The constructor fetches the document API on every call: network failures
raise `OSError`, invalid specifiers and non-JSON responses raise
`ValueError`. Only public notebooks are reachable. Cache the document once
and use `from_observablehq_document` in builds and tests. See Import
ObservableHQ notebooks.
---
## Cell helpers
`Cell` is the immutable authored-cell record. `ojs`, `js`, `md`, and `html`
create the modes used most often from Python.
```python
import observablejs as obs
notebook = obs.Notebook(
obs.md("# Report", key="title"),
obs.ojs("total = rows.length", key="total", display=False),
variables={"rows": [{"x": 1}, {"x": 2}]},
)
```
## `Cell`
```python
Cell(
source,
mode="ojs",
key=None,
display=True,
raw=False,
id=None,
pinned=False,
output=None,
notebookkit_attrs={},
)
```
| Argument | Contract |
| ------------------- | ------------------------------------------------------------------------------------------------- |
| `source` | Cell source string. |
| `mode` | `js`, `ts`, `ojs`, `md`, `html`, `tex`, `dot`, `sql`, `node`, `python`, or `r`. |
| `key` | Nonempty portable public identity. Serialized as `data-pyobservablejs-key`. |
| `display` | `False` emits the Notebook Kit `hidden` attribute. |
| `raw` | `True` preserves indentation and surrounding newlines. |
| `id` | Optional unique positive Notebook Kit serialization id through JavaScript's safe integer maximum. |
| `pinned` | Marks source for display when the notebook enables pinned source. |
| `output` | Notebook Kit output metadata. |
| `notebookkit_attrs` | Typed mode metadata for `database`, `format`, or `since`. |
With `raw=False`, construction dedents the source and strips surrounding
newlines. Invalid source or id types raise `TypeError`. Unsupported modes,
empty keys, unsafe ids, and collisions between first-class fields and
`notebookkit_attrs` raise `ValueError`.
## Helper functions
```python
ojs(source, *, key=None, display=True, raw=False, id=None, pinned=False, output=None, notebookkit_attrs=None) -> Cell
js(source, *, key=None, display=True, raw=False, id=None, pinned=False, output=None, notebookkit_attrs=None) -> Cell
md(source, *, key=None, display=True, raw=False, id=None, pinned=False, output=None, notebookkit_attrs=None) -> Cell
html(source, *, key=None, display=True, raw=False, id=None, pinned=False, output=None, notebookkit_attrs=None) -> Cell
```
The helper fixes the corresponding mode and forwards every other argument to
`Cell`. Observable variable names come from source code. Cell selection uses
the public key.
---
## File attachments
`files` registers named inputs for Observable's `FileAttachment` builtin.
Local files are read during `Notebook` construction and sent to the browser
as data URLs.
```python
from pathlib import Path
import observablejs as obs
data_dir = Path("data")
data_dir.mkdir(exist_ok=True)
(data_dir / "rows.csv").write_text(
"date,value\n2026-07-09,12\n2026-07-10,18\n",
encoding="utf-8",
)
notebook = obs.Notebook(
obs.ojs(
'rows = FileAttachment("rows.csv").csv({typed: true})',
key="rows",
),
files={"rows.csv": "rows.csv"},
base_path=data_dir,
)
```
The mapping key is the name used by `FileAttachment`. In the example,
`typed: true` asks the CSV reader to convert values such as numbers and
dates.
## Accepted `files` values
```python
obs.Notebook(..., files={name: value}, base_path=None)
```
Each value can be one of these forms:
| Value | Resulting attachment record |
| ------------------------------------------------ | -------------------------------------------------------- |
| Local `str` or `pathlib.Path` | A data URL plus inferred `mimeType` and byte `size`. |
| URL string such as `https:`, `data:`, or `blob:` | The URL plus a `mimeType` inferred from the mapping key. |
| Mapping | A normalized record with `url` and optional metadata. |
`FileAttachment` records use camelCase field names.
```python
notebook = obs.Notebook(
obs.ojs('countries = FileAttachment("countries.json").json()'),
files={
"countries.json": {
"url": "https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json",
"mimeType": "application/json",
}
},
)
```
A mapping used by `FileAttachment` requires a string `url`. `mimeType`,
`lastModified`, and `size` are optional. Python keeps these browser attachment
fields in the synchronized record.
## Browser access
`FileAttachment(name)` returns Notebook Kit's browser attachment object. Its
`href`, `name`, `mimeType`, `lastModified`, and `size` properties are always
present. `lastModified` and `size` are `undefined` when the registered
record omits them.
| Data | Methods |
| ----------------------------- | ----------------------------------------------------------- |
| URL | `.href`, `.url()` |
| Raw response | `.blob()`, `.arrayBuffer()`, `.stream()`, `.text()` |
| JSON and delimited text | `.json()`, `.csv()`, `.tsv()`, `.dsv()` |
| Documents and images | `.image()`, `.xml()`, `.html()` |
| Arrow, Parquet, and workbooks | `.arrow()`, `.arquero()`, `.parquet()`, `.zip()`, `.xlsx()` |
`.href` contains the attachment URL. Notebook Kit's deprecated `.url()`
method returns the same value asynchronously. Use `.href` in new cells.
The widget also adds `.sqlite()`, which returns a `SQLiteDatabaseClient`
with `query`, `queryRow`, `sql`, and schema-inspection methods. Before
calling it, set `globalThis.observablejsSqlite` to an object with
`initSqlJs` and an optional `locateFile`, or assign the loader directly to
`globalThis.initSqlJs`.
## Path and base resolution
Local paths are expanded with `Path.expanduser()` and resolved to absolute
paths during construction.
`base_path` supplies a resolution base. It is not a filesystem boundary, so
a relative path containing `..` can resolve outside that directory.
| Call | Base for a relative `files` value |
| ----------------------------------------- | ---------------------------------------------- |
| `Notebook(..., base_path=path)` | `path` |
| `Notebook(..., base_path=None)` | Current working directory at construction time |
| `Notebook.from_html(..., base_path=path)` | `path` |
The local file bytes are captured when the notebook is created. The
registered data URL remains that construction-time snapshot across later
file and current working directory changes.
An explicit local path raises `FileNotFoundError` when it is missing. File
read and metadata failures raise the corresponding `OSError` subclass.
## Attachment records
`notebook.attachments` is the attachment mapping from the latest detached,
recursively read-only `NotebookState` snapshot. For a local file, the record
has this shape:
```python
{
"rows.csv": {
"url": "data:text/csv;base64,...",
"mimeType": "text/csv",
"size": 39,
}
}
```
The MIME type comes from the attachment name. `.arrow` and `.parquet` use
Apache Arrow and Apache Parquet media types. Unknown suffixes use
`application/octet-stream`.
Names absent from `notebook.attachments` resolve against the browser document
base URI.
Frameworks that listen to traitlets can observe `notebook.state` for changes.
Attachments are fixed at construction, so create a new notebook to change them.
## Source HTML discovery
`Notebook.from_html` can discover local attachment calls in Notebook Kit
HTML:
```python
notebook = obs.Notebook.from_html(
source,
base_path="notebooks/report",
embed_file_attachments=True,
)
```
`embed_file_attachments=True` registers existing local files referenced by
literal `FileAttachment("name")` calls inside JavaScript notebook cells.
Static template literals such as ``FileAttachment(`rows.csv`)`` and imported
aliases from `observablehq:stdlib` are also recognized. Discovery ignores
calls in comments, string literals, regular-expression literals, Markdown
cells, and scripts outside the `` element.
Discovery populates `notebook.attachments` with data URLs and keeps each
`FileAttachment` call in the imported document definition. Explicit `files`
entries take precedence when they use the same name. Each discovered name
whose resolved path is an existing file becomes a record. Remaining names
use browser URL resolution.
`base_path` is required when `embed_file_attachments=True`. Pass the
directory that should resolve relative attachment names. See Source
constructors for the source constructor contract and
relative JavaScript import rewriting.
## Serialized HTML boundary
`notebook.to_notebook_html()` returns the Notebook Kit document text, not
its attachment records. When that text is consumed independently, its
environment must provide the URLs used by `FileAttachment`, or the source
must use URLs that remain reachable there.
---
## API reference
```python
import observablejs as obs
```
The top-level package exports the runtime objects and cell helpers:
| Export | Contract |
| --------------------------------------------------------------------- | ---------------------------------------------------------------- |
| `Notebook` | Controls a notebook definition and Python-owned state. |
| `NotebookView` | Renders selected cells and exposes one consistent browser state. |
| `view_from_*` | Builds a full renderable view through a standalone factory. |
| `NotebookCell` | Identifies one notebook-owned cell by key. |
| `Cell` | Describes one authored cell. |
| `ojs`, `js`, `md`, `html` | Create common cell modes. |
| `NotebookGraph` | Contains cells and symbolic dependencies. |
| `CellInfo` | Describes one graph cell. |
| `DependencyEdge` | Links source and target cells through one variable. |
| `NOTEBOOK_THEMES` | Lists accepted Notebook Kit themes. |
| `types` | Namespaces advanced typed mappings and state records. |
Use Source constructors for imported notebooks,
File attachments for `files`, and Variables and
serialization for state conversion.
---
## NotebookView and NotebookCell
# `NotebookView` and `NotebookCell`
Call `Notebook.view(...)` to create a renderable view backed by a
`NotebookView`. `Notebook.cell` and `Notebook.cells` return stable
`NotebookCell` handles.
## Standalone view factories
The standalone factories construct a notebook and return its full renderable
view. The view keeps its controller available as `view.notebook` and owns that
notebook session. Calling `view.close()` closes the view and session.
```python
import observablejs as obs
view = obs.view_from_code("viewof n = Inputs.range([0, 10])")
```
```python
view_from_code(
code,
*,
mode="ojs",
title="Untitled",
theme="air",
files=None,
base_path=None,
variables=None,
**view_options,
) -> NotebookView
view_from_html(
source,
*,
files=None,
base_path=None,
embed_file_attachments=False,
rewrite_imports=False,
variables=None,
show_pinned_source=False,
**view_options,
) -> NotebookView
view_from_observablehq(
specifier,
*,
variables=None,
files=None,
show_pinned_source=False,
timeout=30,
**view_options,
) -> NotebookView
view_from_observablehq_document(
document,
*,
title=None,
variables=None,
files=None,
show_pinned_source=False,
**view_options,
) -> NotebookView
```
The imported-source factories accept the same keyword options as their
matching `Notebook.from_*` constructors. `view_from_code` accepts notebook
title, theme, files, base path, and initial variables. The `specifier` may be a
public ObservableHQ URL, slug, id, or document API URL. `document` accepts an
`observablejs.types.ObservableDocument` mapping.
Each factory accepts the typed `Notebook.view()` options through
`**view_options`. `capture_state` matches the option on `Notebook.view()`. Set
it to `False` when the rendered output is all you need and Python will not read
`NotebookView.state`. The view remains interactive and its state stays at its
initial value.
Cell, notebook, file, and network errors come from the matching constructor.
Invalid view options raise `TypeError` before file or network access. See Skip
Python state capture
for guidance.
## `NotebookCell`
```python
cell = notebook.cell("answer")
print(cell.key, cell.index, cell.id)
```
| Member | Contract |
| ------- | ---------------------------------------------------------- |
| `key` | Portable public identity, or `None` for an anonymous cell. |
| `index` | Zero-based notebook order metadata. |
| `id` | Notebook Kit serialization metadata. |
Use the key or handle as a `Notebook.view(...)` selector. The id and index do
not select cells.
## `NotebookView`
The underlying `NotebookView` is an anywidget model with one browser run. It
evaluates its selected cells and any other cells they need.
```python
view = notebook.view("answer")
view
```
In a running marimo notebook, `Notebook.view()` returns a marimo UI element
that proxies the underlying `NotebookView`. Use the returned view directly in
cell output and layouts. Other anywidget hosts receive the `NotebookView`
model directly.
| Member | Contract |
| ---------- | -------------------------------------------------- |
| `notebook` | The owning `Notebook` controller. |
| `cells` | Selected `NotebookCell` handles in notebook order. |
| `state` | Current immutable `ViewState` browser snapshot. |
Before a browser mounts the view, `state.input_revision` and
`state.settled_revision` are `None`, `pending` is false, and no results or graph
exist. Each accepted browser snapshot replaces `state` once. Frameworks that
listen to traitlets can observe it:
```python
def on_view_state(change):
state = change["new"]
if not state.pending:
print(state.results)
view.observe(on_view_state, names="state")
```
With `capture_state=False`, `state` remains at this initial value. Rendering,
Python variable updates, and supported input synchronization continue.
One `NotebookView` can have one live writable render at a time. Create another
view from the notebook when the same selection must appear in two outputs.
## `NotebookView.close()`
For a view created by `Notebook.view()`, this closes the view and its browser
runtime while the notebook session remains active. A view returned by a
standalone factory owns its notebook session, so closing it also closes any
other views created from `view.notebook`. Repeated calls are no-ops. Late
browser callbacks cannot publish new state after close.
See View state and graph for the result, error, revision,
and dependency records.
---
## Notebook
# `Notebook`
`Notebook` is a traitlets controller for one notebook definition and the values
set from Python. It creates renderable views backed by `NotebookView` objects.
## Constructor
```python
Notebook(
*cells,
title="Untitled",
theme="air",
files=None,
base_path=None,
variables=None,
show_pinned_source=False,
)
```
```python
import observablejs as obs
notebook = obs.Notebook(
obs.md("# Summary", key="title"),
obs.ojs("answer = 40 + 2", key="answer"),
variables={"precision": 2},
)
```
| Argument | Contract |
| -------------------- | --------------------------------------------------------------------- |
| `*cells` | `Cell` objects from the public helpers or direct `Cell` construction. |
| `title` | Notebook Kit document title. |
| `theme` | Theme name or typed light and dark theme pair. |
| `files` | Mapping of attachment names to URLs, paths, or typed file records. |
| `base_path` | Base directory for relative paths in `files`. |
| `variables` | Initial mapping of Python-owned Observable variables. |
| `show_pinned_source` | Shows source for selected cells marked as pinned. |
Invalid cell types, mappings, or serializable values raise `TypeError`.
Duplicate cell ids or keys, invalid variable names, and unsupported themes
raise `ValueError`. Local attachment access may raise `OSError`.
## `Notebook.view(*selectors, **options)`
Returns a new renderable view. With no selectors, it renders every cell. In a
running marimo notebook, the return value is a marimo UI element that proxies
its `NotebookView`.
```python
import observablejs as obs
answer = obs.ojs("answer = 40 + 2", key="answer")
double = obs.ojs("double = answer * 2", key="double")
notebook = obs.Notebook(answer, double)
full_view = notebook.view()
answer_view = notebook.view("answer")
mixed_view = notebook.view(double, notebook.cell("answer"))
preview = notebook.view(capture_state=False)
```
Each positional selector is one of:
- a string public cell key
- an authored `Cell` with a key
- a `NotebookCell` owned by this notebook
Each selector resolves to one cell, then cells render in notebook order.
Unknown keys raise `KeyError`. Authored `Cell` selectors need a key, and
`NotebookCell` selectors must belong to the notebook. Duplicate selections and
foreign handles raise `ValueError`. Invalid selector types raise `TypeError`.
Every view has its own browser run and `state` snapshot. Views from the same
notebook receive the same Python variables and supported browser input values.
`options` is typed as `NotebookViewOptions`. `capture_state` controls whether
browser evaluation updates `NotebookView.state` and defaults to `True`. Set it
to `False` when the rendered output is all you need and Python will not read
its state. The view remains interactive and `state` stays at its initial value.
Unknown options and non-boolean `capture_state` values raise `TypeError`. See
Skip Python state
capture for performance
guidance.
## `Notebook.cell(key)`
Returns the stable `NotebookCell` for one unique string key.
```python
answer = notebook.cell("answer")
```
Unknown keys raise `KeyError`. Non-string selectors raise `TypeError`. The
Notebook Kit `id` and notebook-order `index` are metadata and are not selectors.
## `Notebook.cells`
Returns all `NotebookCell` handles in notebook order. Anonymous cells remain
available in this tuple.
## Controller state
`Notebook.state` is a separate, deeply read-only `NotebookState` snapshot. It
contains `variables`, `attachments`, and `theme`.
```python
def on_state(change):
print(change["new"].variables)
notebook.observe(on_state, names="state")
```
`Notebook.variables`, `Notebook.attachments`, and `Notebook.theme` read from the
latest snapshot. Changing an object passed during construction does not change
the notebook. Snapshot values cannot be edited. Use the variable methods or
assign `notebook.theme` for changes. Each real change publishes one `state`
event. Sending the same serialized value publishes none.
An unchanged variable update can still clear a browser-owned input with the
same name and reapply the Python value. The controller snapshot stays the same.
Source-backed notebooks read their theme from source HTML. Assigning a theme
to one raises `traitlets.TraitError`.
## Variable mutation
### `Notebook.update_variables(values, /)`
Merges exactly one `Mapping[str, object]` into the Python-owned environment.
```python
notebook.update_variables({"threshold": 0.8})
```
### `Notebook.replace_variables(values, /)`
Replaces the environment with exactly one mapping. Omitted names return to
their notebook-defined values.
```python
notebook.replace_variables({"rows": [{"x": 1}, {"x": 2}]})
```
### `Notebook.reset_variables(*names)`
Releases the listed Python-owned names. Unknown names and an empty call are
no-ops.
All three methods return `None`. Invalid or reserved variable names raise
`ValueError`. Non-mapping update and replacement arguments or unsupported
values raise `TypeError`.
## `Notebook.to_notebook_html()`
Returns the definition as Notebook Kit HTML. Public cell keys use the
`data-pyobservablejs-key` script attribute. Controller variables remain
session state.
## `Notebook.close()`
Closes the shared session and every live view. Repeated calls are no-ops.
Creating a view or mutating a closed notebook raises `RuntimeError`.
See Source constructors for `from_html`,
`from_observablehq`, and `from_observablehq_document`.
---
## Source constructors
Source constructors return a `Notebook` with the same view, variable, state,
and close APIs as a Python-authored notebook. Imported cells and remote modules
run with the host page's browser privileges. Read the trust boundary in
Browser execution.
## `Notebook.from_html`
```python
Notebook.from_html(
source,
*,
files=None,
base_path=None,
embed_file_attachments=False,
rewrite_imports=False,
variables=None,
show_pinned_source=False,
) -> Notebook
```
Creates a source-backed notebook from Notebook Kit HTML text. The notebook
theme comes from the HTML. Each `data-pyobservablejs-key` becomes the public
cell key.
```python
from pathlib import Path
import observablejs as obs
path = Path("notebooks/report.html")
notebook = obs.Notebook.from_html(
path.read_text(encoding="utf-8"),
base_path=path.parent,
embed_file_attachments=True,
rewrite_imports=True,
)
```
- `source` is a string containing Notebook Kit HTML.
- `files` registers explicit attachment URLs, paths, or typed file records.
- `base_path` resolves relative attachment and module paths.
- `embed_file_attachments` embeds referenced local files as data URLs.
- `rewrite_imports` embeds quoted relative JavaScript modules.
- `variables` sets the initial Python-owned variable mapping.
- `show_pinned_source` displays source for cells marked as pinned.
The constructor raises `TypeError` for an invalid source type or file mapping.
It raises `ValueError` for malformed notebook metadata, duplicate cell ids or
keys, unsupported themes, and invalid local import graphs. File reads may raise
`OSError` or `UnicodeError`.
## `Notebook.from_observablehq`
```python
Notebook.from_observablehq(
specifier,
*,
variables=None,
files=None,
show_pinned_source=False,
timeout=30,
) -> Notebook
```
Fetches a public ObservableHQ notebook through its document API. The specifier
may be a notebook URL, slug, id, or document API URL.
```python
import observablejs as obs
notebook = obs.Notebook.from_observablehq("@d3/bar-chart", timeout=10)
```
Remote uploaded files become URL-backed attachments. Explicit `files` replace
records with the same file name. The fetched document id and version pin its
Observable notebook imports. `timeout=None` disables the per-request timeout.
Invalid specifiers and document shapes raise `ValueError`. Network failures
raise `OSError`. Response decoding may raise `UnicodeError`.
## `Notebook.from_observablehq_document`
```python
Notebook.from_observablehq_document(
document,
*,
title=None,
variables=None,
files=None,
show_pinned_source=False,
) -> Notebook
```
Creates a notebook from an existing `observablejs.types.ObservableDocument`.
ObservableHQ node names become public cell keys at this adapter boundary.
```python
import observablejs as obs
document: obs.types.ObservableDocument = {
"id": "1234567890abcdef",
"version": 7,
"title": "Report",
"nodes": [
{"id": 1, "mode": "js", "name": "answer", "value": "answer = 42"},
],
}
notebook = obs.Notebook.from_observablehq_document(document)
```
`title=None` uses the document title and then `"Untitled"`. Document files
become URL-backed attachments, with explicit `files` taking precedence.
Preserve a valid source `id` and `version` when imported Observable notebooks
must keep the source revision's dependency resolution.
The document must be a mapping with a node sequence. Unsupported node modes,
unsafe or duplicate ids, duplicate public keys, and malformed node records
raise `TypeError` or `ValueError`.
See `observablejs.types` for the document, node, file, source,
data, and display records.
---
## Themes
`theme` accepts one Notebook Kit theme name.
```python
import observablejs as obs
notebook = obs.Notebook(
obs.md("# Report"),
theme="glacier",
)
```
The default is `"air"`. A light and dark mapping selects a theme for each
browser color scheme.
```python
notebook = obs.Notebook(
obs.md("# Report"),
theme={"light": "cotton", "dark": "slate"},
)
```
`observablejs.NOTEBOOK_THEMES` is a tuple containing the accepted names:
```python
(
"air",
"coffee",
"cotton",
"deep-space",
"glacier",
"ink",
"midnight",
"near-midnight",
"ocean-floor",
"parchment",
"slate",
"stark",
"sun-faded",
)
```
Theme names are stripped and normalized to lowercase. Unknown names raise
`ValueError`. Mappings must contain exactly `light` and `dark`, with a valid
theme name for each value. Other value types raise `TypeError`.
Source-backed notebooks carry their theme in the ``
attribute, including the `light-dark(a, b)` mapping form. Assigning
`notebook.theme` on a source-backed notebook raises `traitlets.TraitError`.
See Style notebooks and show
source.
---
## observablejs.types
# `observablejs.types`
`observablejs.types` publishes annotations for advanced inputs and synchronized
state.
```python
import observablejs as obs
file: obs.types.FileSpec = {
"url": "https://example.test/data.csv",
"mimeType": "text/csv",
}
document: obs.types.ObservableDocument = {
"title": "Report",
"nodes": [{"id": 1, "mode": "js", "value": "answer = 42"}],
}
```
## Input contracts
| Type | Contract |
| --------------------------------------------------------- | --------------------------------------------------------------- |
| `CellMode`, `CellFormat` | Supported Notebook Kit cell modes and file formats. |
| `CellSelector` | String key, authored `Cell`, or notebook-owned `NotebookCell`. |
| `NotebookViewOptions` | Keyword options shared by `Notebook.view()` and view factories. |
| `NotebookTheme`, `ThemePair`, `Theme` | Theme names and light or dark pairs. |
| `FileSpec`, `FileInput` | File input record, URL, or filesystem path. |
| `NotebookKitCellMetadata` | Mode metadata for database, format, and since. |
| `ObservableDocument`, `ObservableNode`, `ObservableFile` | ObservableHQ document API records. |
| `ObservableSource`, `ObservableData`, `ObservableDisplay` | Nested ObservableHQ source and display records. |
`NotebookViewOptions` currently accepts one optional key:
| Key | Type | Default | Contract |
| --------------- | ------ | ------- | -------------------------------------------------- |
| `capture_state` | `bool` | `True` | Synchronizes browser evaluation state with Python. |
Runtime parsing remains defensive when JSON data does not match an annotation.
## State contracts
`CellStatus` is `pending`, `success`, or `error`. `ErrorPhase` is `analysis`,
`evaluation`, `rendering`, or `serialization`.
The immutable records are:
- `NotebookState`
- `ViewState`
- `CellResult`
- `CellError`
- `ViewError`
- `BrowserErrorValue`
`FileSnapshot` and `ThemeSnapshot` describe the read-only mappings inside
`NotebookState`.
See Notebook for controller snapshots and
View state and graph for browser revisions and results.
---
## View state and graph
`NotebookView.state` is one consistent, read-only snapshot of browser work.
```python
state = view.state
if (
not state.pending
and state.input_revision is not None
and state.settled_revision == state.input_revision
):
result = state.result("chart")
```
## `ViewState`
| Field | Type | Contract |
| ------------------ | ------------------------ | ------------------------------------------------- |
| `input_revision` | optional `int` | Latest browser update number. |
| `settled_revision` | optional `int` | Latest update where every selected cell finished. |
| `pending` | `bool` | Whether the latest update is still running. |
| `results` | `tuple[CellResult, ...]` | Selected-cell results in notebook order. |
| `errors` | `tuple[ViewError, ...]` | Setup or view-level failures. |
| `graph` | optional `NotebookGraph` | Current read-only dependency graph snapshot. |
Before display, both revisions are `None`. The first browser run uses revision
`0`. Python updates, browser inputs, and later generator values advance
`input_revision`. While work runs, `settled_revision` stays at the previous
finished revision. Results from older revisions are ignored.
### `ViewState.result(selector)`
Returns the result for a key string, keyed authored `Cell`, or `NotebookCell`
from the same notebook. Missing results raise `KeyError`, including requests
made before the browser starts. Invalid selector types raise `TypeError`. Once
the state contains results, a handle from another notebook raises `ValueError`.
## `CellResult`
| Field | Type | Contract |
| ---------- | ----------------------- | ------------------------------------------ |
| `cell` | `NotebookCell` | Stable selected-cell handle. |
| `revision` | `int` | Input revision represented by this result. |
| `status` | `CellStatus` | `"pending"`, `"success"`, or `"error"`. |
| `values` | `Mapping[str, object]` | Separate, read-only output values. |
| `errors` | `tuple[CellError, ...]` | Structured failures for this cell. |
`success` means every expected output finished. An `error` result may still
contain values from outputs that succeeded. Read results after the current
revision finishes.
## Structured errors
`CellError` contains `name`, `message`, `phase`, and an optional output
`variable`. `ViewError` contains `name`, `message`, and `phase`. The phase is
`analysis`, `evaluation`, `rendering`, or `serialization`. Browser stack text
is not part of the contract.
A JavaScript expression that returns `new Error("invalid value")` has
succeeded. Its value becomes `BrowserErrorValue(name, message)`. A rejected
evaluation produces `CellError` and an error result.
## `NotebookGraph`
| Member | Contract |
| --------------------- | ---------------------------------------------------- |
| `cells` | Immutable `CellInfo` records. |
| `edges` | Immutable `DependencyEdge` records. |
| `defines` | Unique defined names in cell order. |
| `references` | Unique referenced names in cell order. |
| `external_references` | Referenced names absent from evaluated cell outputs. |
### `NotebookGraph.cell(key)`
Returns the unique keyed `CellInfo`. Anonymous cells remain available through
`graph.cells`. Unknown or ambiguous keys raise `KeyError`.
### `NotebookGraph.cell_for_variable(variable)`
Returns the unique cell that defines a variable. Missing or ambiguous
definitions raise `KeyError`.
### Diagram export
`graph.to_mermaid()` returns a Mermaid `flowchart LR` string.
`graph.to_d2()` returns a D2 diagram with rightward layout. Diagram labels use
the public key and fall back to notebook position for anonymous cells.
## `CellInfo`
`CellInfo` exposes `id`, `index`, `mode`, `key`, `defines`, `references`,
`output`, `outputs`, `runtime_outputs`, `autodisplay`, `autoview`,
`automutable`, and optional `error` fields. The id and index are metadata.
## `DependencyEdge`
Each edge contains `source` and `target` `CellInfo` objects plus the linking
`variable` name.
See Inspect the dependency graph for a live
workflow.
---
## Variables and serialization
`variables` sends named Python values to the Observable notebook.
```python
import observablejs as obs
notebook = obs.Notebook(
obs.ojs("md`threshold is ${threshold}`", key="readout"),
variables={"threshold": 0.75},
)
```
## Variable names
Names must match `^[A-Za-z_$][0-9A-Za-z_$]*$`. Observable runtime builtins such
as `Inputs`, `Plot`, `FileAttachment`, `width`, and `invalidation` are reserved.
ObservableHQ sources also reserve classic runtime names such as `require`.
Invalid or reserved names raise `ValueError`.
## Write variables
```python
notebook.update_variables({"threshold": 0.8})
notebook.replace_variables({"rows": rows})
notebook.reset_variables("threshold")
```
`update_variables(values, /)` merges exactly one mapping.
`replace_variables(values, /)` replaces the complete mapping sent from Python.
`reset_variables(*names)` removes selected Python values so the notebook can
define them again. Keyword updates, pair iterables, and `None` are invalid for
update and replacement.
Sending the same serialized value does not emit a controller `state` event. It
is also a browser no-op unless the browser has changed a shared input with that
name. In that case, the update clears the browser-owned value and reapplies the
Python value. A real change reaches every active view.
## Read controller variables
`Notebook.variables` is the variable mapping from the latest
`NotebookState`. It is detached from construction inputs and recursively
read-only. After serialization, sequences are tuples, nested mappings are
read-only, byte-like values are bytes, and DataFrame-like inputs are row records.
```python
def on_state(change):
print(change["new"].variables)
notebook.observe(on_state, names="state")
```
Mutating the snapshot cannot update the notebook. Use the mutation methods for
writes.
## Python to browser conversion
| Python input | Browser value |
| ------------------------------------ | -------------------------- |
| `None`, `bool`, `str` | `null`, boolean, string |
| Safe `int`, finite `float` | number |
| Large `int` | `BigInt` |
| `NaN` and infinities | matching JavaScript number |
| `date`, `datetime` | `Date` |
| `bytes`, `bytearray`, `memoryview` | `Uint8Array` |
| Mapping | plain object |
| Range, sequence, iterable | array |
| pandas or Polars DataFrame | array of row objects |
| pandas or Polars Series, NumPy array | array |
One-shot iterators materialize once. Unsupported values raise `TypeError`
before state changes. Use file attachments for data
that should load through `FileAttachment`.
## Shared `viewof` inputs
When a Python variable and named `viewof` input use the same name, the initial
Python value sets the control. Browser changes then update sibling views. A
later Python update sets the value again in every view. Use the same input shape
for a shared name across views.
## Browser result conversion
Cell values in `NotebookView.state` decode into detached read-only mappings.
| Browser value | Python result value |
| ----------------------------------------- | -------------------------- |
| `undefined`, `null` | `None` |
| Number, boolean, string | JSON-compatible scalar |
| `BigInt` | `int` |
| Valid `Date` | `datetime.datetime` |
| Array, plain object | tuple or read-only mapping |
| `Map`, `Set` | tuple-based records |
| Array buffer or typed array | `bytes` |
| `Error` returned as data | `BrowserErrorValue` |
| DOM element, function, regular expression | stable summary string |
| `File`, `Blob` | read-only metadata mapping |
Evaluation, rendering, and serialization failures appear in structured
`CellError` records. See View state and graph.