# 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 `<notebook>` 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 <notebook theme="light-dark(cotton, slate)"> <script type="text/markdown"> # Report </script> </notebook> ``` 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`<p> ${numbers.length} values, ending at <strong>${numbers.at(-1) * scale}</strong>. </p>` """, 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`<p>Threshold: <strong>${threshold}</strong></p>`', 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`<p>Threshold: <strong>${threshold}</strong></p>`', 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`<p>Doubled: <strong>${doubled}</strong></p>`', 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`<strong>Threshold: ${threshold}</strong>`', 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 `<notebook theme="...">` 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 `<notebook>` 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 `<notebook theme="...">` 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.