Headless Python
notebook.data evaluates notebook cells inside the Python process and returns
their values as Python objects, Arrow tables, or dataframes. Install the package
with the dataframe library you want to use:
pip install pyobservablejs polars
import observablejs as obs
with obs.Notebook(
obs.js("const sales = [{amount: price * 2}]", key="result"),
variables={"price": 7},
) as notebook:
frame = notebook.data["sales"].to_polars()
print(frame)
notebook.update_variables({"price": 9})
print(notebook.data["sales"].to_python()) # [{'amount': 18}]
Reads evaluate the producing cell and its dependencies and return the requested
Python object directly. The notebook owns its lazy execution resources. Use its
context manager or call notebook.close() when finished.
notebook.with_variables(price=9) creates an independent notebook binding.
The original notebook and its views retain their current variables.
Embedded engine
Each execution runs in its own V8 JavaScript isolate inside
the Python process, with a Happy DOM
document for HTML, SVG, and input construction. The engine has no browser layout
or graphics. Its width is 640 and dark is false.
Native wheels carry the engine for Linux x86_64 and aarch64 with glibc 2.28 or
newer, macOS 12 or newer on Apple silicon and Intel, and Windows x86_64. Other
platforms, including Pyodide in marimo WebAssembly
notebooks and JupyterLite, install the pure Python wheel. It displays views and
reads their data through view.data. Headless reads
from the pure wheel raise ImportError.
A timeout terminates the affected execution, including a synchronous JavaScript
loop. A subsequent unpinned read starts a new execution. Exceptions retain
JavaScript stacks, source excerpts, and structured diagnostics. An execution that
exceeds its 4 GiB JavaScript heap stops with errors.EngineError.
Engines belong to the process that started them. A child process created with
fork after its parent ran headless reads must start with the spawn
start method
to use notebook.data.
Supply Python-owned values to bypass input construction:
with obs.Notebook(
obs.ojs("viewof threshold = Inputs.range([0, 1])"),
obs.ojs("answer = threshold * 2"),
variables={"threshold": 0.5},
) as notebook:
print(notebook.data["answer"].to_python()) # 1
Inspect and discover
names = notebook.data.names()
upstream = notebook.graph.upstream("result")
catalog = notebook.data.discover()
Names and graph inspection compile source without executing cells or fetching
imports. Discovery evaluates values and returns actionable dataset references,
diagnostics, and a pending flag. See the
data namespace reference.
Network and imports
Headless reads can fetch attachments, modules, and public Observable notebook imports by default:
with obs.Notebook.from_observablehq("@d3/sized-donut-multiples") as notebook:
frame = notebook.data["data"].to_polars()
Use network=False for offline execution, or a host list to restrict requests:
offline = notebook.data.using(network=False)
restricted = notebook.data.using(
network=["cdn.jsdelivr.net", "static.observableusercontent.com"]
)
A host entry matches every port of that host. Write host:port to allow one
port. The policy applies to fetch, attachments, and module imports. The
embedded engine also checks every redirect target.
The embedded engine keeps downloaded module source in the user cache directory for the
lifetime its server declares through Cache-Control: max-age. Later processes
read fresh entries from that cache. Set PYOBSERVABLEJS_CACHE_DIR to choose
another directory. Classic require() loads packages through native ESM
entries while preserving declared package versions. Use Chromium for libraries
that require AMD globals or browser-specific behavior.
Supply local notebook dependencies through resolve_notebook:
dependency = obs.Notebook(obs.ojs("factor = 3"))
main = obs.Notebook(
obs.ojs('import {factor} from "@example/constants"'),
obs.ojs("answer = factor * 7"),
)
def resolve(specifier):
if specifier == "@example/constants":
return dependency
raise KeyError(specifier)
data = main.data.using(resolve_notebook=resolve)
print(data["answer"].to_python()) # 21
main.close()
dependency.close()
The resolver runs on a background thread. Return an open notebook. The caller owns those dependency notebooks. The runtime caches source within an execution and resolves version pins before calling the resolver.
Chromium
Use Chromium for canvas, layout, and browser APIs. Install the chromium extra,
which adds Playwright, a browser automation
library:
pip install 'pyobservablejs[chromium]'
data = notebook.data.using(engine="chromium")
frame = data["sales"].to_polars()
png = notebook.render.png("chart", scale=2, timeout=120)
scale defaults to 1.0 and accepts positive finite numbers. A scale of 2
doubles the PNG width and height while preserving the notebook's CSS layout.
Fractional scales such as 1.5 are supported.
Playwright downloads its matching Chromium build on first use. Engine preparation has a separate three-minute deadline, so the initial download does not consume the read timeout. Each execution uses a fresh browser context and closes the browser with the notebook. The network policy applies to Chromium page requests and WebSockets. Browser installation is separate from notebook network access.
Engine selection is explicit. A failing embedded computation is not reexecuted in Chromium automatically, which could repeat its side effects.
Async and widget access
Synchronous reads are the default. Use notebook.data.aio or
notebook.files.aio when an async application should yield while waiting:
frame = await notebook.data.aio["sales"].to_polars()
A displayed widget has its own awaitable namespace:
frame = await view.data["sales"].to_polars()
Widget construction, display, and widget data access work with every wheel. They read the originating view's browser values, including its current interactions.