Anywidget: Custom Jupyter Widgets Made Easy
Anywidget is a Python library that dramatically simplifies the process of creating custom interactive widgets for computational notebooks, running in environments like Jupyter, JupyterLab, Google Colab, VS Code, and Marimo. It's both a specification and a toolkit, allowing developers to define widget front-end code using standard ECMAScript modules (ESM) within a Python class. Currently at version 0.10.0, it features a rapid release cadence with frequent updates.
Warnings
- breaking Since v0.9, the preferred way to define front-end widget code has shifted to using lifecycle hooks (`initialize`, `render`) exported as a default object from the `_esm` module, replacing the direct `export function render(view)` pattern.
- breaking If you are using the Vite plugin for `anywidget`, the import path for it changed from `anywidget/vite` to `@anywidget/vite` to allow for independent versioning.
- gotcha Hot Module Replacement (HMR) for live development requires opting in by either setting the `ANYWIDGET_HMR` environment variable to `1` or, preferably, referencing frontend code via `pathlib.Path` for `_esm` and `_css` attributes.
- gotcha The `__repr__` method for `AnyWidget` subclasses was overridden to provide a less verbose, `object.__repr__` like output, instead of the full serialization of all trait values inherited from `ipywidgets.Widget`.
- gotcha Custom messages sent from Python (`widget.send()`) will only be received by the frontend if the widget's view has already been rendered (i.e., the widget has been `display()`ed). Sending messages before the widget is displayed will result in them being lost.
- breaking For `anywidget` framework bridges like `@anywidget/react` and `@anywidget/svelte`, there have been breaking changes to align with newer versions of their respective frameworks (React 18's `useSyncExternalStore` and Svelte 5's runes reactivity). This primarily affects users building widgets with these specific frontend frameworks.
Install
-
pip install "anywidget[dev]" -
pip install anywidget -
conda install -c conda-forge anywidget
Imports
- AnyWidget
from anywidget import AnyWidget
- Int
import traitlets class MyWidget(AnyWidget): value = traitlets.Int(0).tag(sync=True) - Unicode
import traitlets class MyWidget(AnyWidget): text = traitlets.Unicode('Hello').tag(sync=True)
Quickstart
import anywidget
import traitlets
from IPython.display import display
class CounterWidget(anywidget.AnyWidget):
_esm = """
export default {
initialize({ model }) {
// Optional: run once per widget instance
console.log('Widget initialized');
},
render({ model, el }) {
let getCount = () => model.get('count');
let button = document.createElement('button');
button.innerHTML = `count is ${getCount()}`;
button.addEventListener('click', () => {
model.set('count', getCount() + 1);
model.save_changes();
});
model.on('change:count', () => {
button.innerHTML = `count is ${getCount()}`;
});
el.appendChild(button);
return () => {
// Optional: cleanup on view removal
button.removeEventListener('click', () => {});
};
}
};
"""
_css = """ button { padding: 5px 10px; border-radius: 5px; background-color: #f0f0f0; } """
count = traitlets.Int(0).tag(sync=True)
widget = CounterWidget()
display(widget)
# You can also interact with the widget from Python
# widget.count = 5 # This will update the frontend