Skip to main content

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.