A Year with anywidget
By Trevor Manz
The anywidget community is growing! Join us on Discord 🐣
TL;DR: anywidget v0.9 introduces initialize
and render
lifecyle
hooks to allow greater control of front-end widget behavior. The preferred
way to define widgets is now with a default
object export:
pip install --upgrade "anywidget[dev]"
export default {
initialize({ model }) {
/* ... */
},
render({ model, el }) {
/* ... */
},
};
anywidget v0.9
We are excited to announce the release of anywidget v0.9! This version introduces a redesigned front-end API that enables more widget types in Jupyter. This update also requires developers to opt-in to live development features, reducing some minor production issues for widgets.
Mimimizing Friction in Jupyter Front Ends
anywidget aims to make creating and sharing Jupyter Widgets as simple as possible. We expose a narrower set of web-standard APIs, compared to traditional Jupyter Widgets, to ensure widgets are more consistent (and easier to debug) across notebook environments.
As a newer library, we’ve had the opportunity to learn from existing use cases and understand dependencies of widgets within wider Jupyter ecosystem. We found that compatibility issues often stem from inconsistencies in notebook front ends, resulting in mismatches with the APIs developers expect. anywidget focuses on a minimal set of APIs we identified as essential for notebook integration, boiling down to:
- Communicating with objects in the Jupyter Kernel
- Modifying notebook output cells (i.e., the DOM)
This strategy sometimes requires widget developers to write extra code, but it ensures better introspection and compatibility. While adopting such APIs at this stage would be very challenging for traditional Jupyter Widgets, anywidget can standardize certain aspects to make widget development less error-prone and more accessible.
Despite these goals, serveral several community members highlighted specific instances where anywidget’s existing API was too restrictive (#266, #388), prompting us to reevaulate our design choices. We began by examining the lifetime of Jupyter Widgets to guide this revision.
The Widget Lifecycle
Jupyter Widgets adhere to a Model View Controller (MVC) pattern in the front end. Within Jupyter’s MVC implementation, we find that there are two distinct steps in a widget’s lifetime:
- Model Initialization: On instantiation in Python, a matching front-end model is created and synced with a model in the kernel.
- View Rendering: Each notebook cell displaying the widget renders an independent view based on the model’s current state.
In anywidget, view rendering logic is defined with render
, but
historically model initialization was handled implicitly. While this
auto-initialization is sufficient for most widgets, in advanced cases it can be
useful to define custom model initialization logic. For example, a widget
might need to register a single event handler or create some front-end only
state to share across views.
In the absence of a dedicated API in anywidget for model initialization,
we surveyed traditional Jupyter Widgets to find where such behavior typically is
defined. We found developers typically extend
DOMWidgetModel.initialize
to define custom model initialization logic, and have adopted these semantics
in our new API.
Introducing Widget Lifecycle Hooks
In anywidget v0.9, the preferred way to define a widget’s front-end code is
now with a default
object export specifying one or more widget lifecycle
hooks:
export default {
initialize({ model }) {
/* ... */
},
render({ model, el }) {
/* ... */
},
};
Combined, these hooks introduce finer control for widget developers.
initialize
: is executed once per widget instance, during model initialization. It has access tomodel
to setup non-view event handlers or state to share across views.render
: is executed once per view, or during view rendering. It has access to both themodel
and ael
DOM element. This method should be familiar and is used to setup event handlers or access state specific to that view.
The default
export may also be a function that returns this interface. This
can be useful to setup some front-end specific state for the lifecycle of the
widget.
export default () => {
// Create a history of all the changes to the "value" trait
let valueHistory = [];
return {
initialize({ model }) {
// Push the new changes to history
model.on("change:value", () => valueHistory.push(model.get("value")));
},
render({ model, el }) {
el.innerText = `The history is ${valueHistory}`;
// Update each view to display the current history
model.on("change:value", () => {
el.innerText = `The history is ${valueHistory}`;
});
},
};
};
Note: Similar to
render
,initialize
can optionally return a callback that is executed at the end of the widget’s lifetime.
Migration
To start using anywidget v0.9, first upgrade your package using pip:
pip install --upgrade "anywidget[dev]"
The new API comes with a deprecation notice for existing named render
exports.
To migrate, please replace:
export function render({ model, el }) {/* ... */}
with:
function render({ model, el }) {/* ... */}
export default { render };
Other Changes
In v0.9, developers need to opt-in to anywidget’s live development features, using an environment variable. You can set this within a notebook:
%env ANYWIDGET_HMR=1
or when launching a Jupyter session:
ANYWIDGET_HMR=1 jupyter lab
anywidget’s builtin file-watching and hot module replacement (HMR) are only intended to be used by developers, but the heuristics for enabling these features lead to many false positives. This caused issues in various production settings (read: a bad experience for end-users), hence our decision to make them explicitly enabled going forward.
Community Highlights and Updates
To wrap up this post, I wanted to share some project and community highlights since anywidget v0.1. It’s been exciting to see the warm reception and growth of the project in the past year. (I likely owe thanks to many of you still reading this.)
Some highlights:
- We launched a Discord for the anywidget community – Join us!
- I contributed a post about anywidget for the Jupyter blog
- The JavaScript Jupyter Widget cookiecutter is now deprecated and recommends anywidget to beginners!
- VS Code has some special logic to ensure anywidget just works 🫠
I’ve also enjoyed following projects that have incorporated anywidget. Too many to list in total, but to spotlight a few:
- Vega-Altair: declarative (interactive) statistical visualization in Python
- jupyter-scatter: interactive 2D scatter plots that scale to millions of points and support view linking
- Mosaic: extensible framework for linking interactive views to databases for scalable data processing
- pyobsplot: a Python interface for Observable Plot that supports Pandas and Polars dataframes
- lonboard: fast, interactive geospatial vector data visualization
In the year to come, I plan to focus on improving anywidget’s documentation and record video tutorials to help beginners get started with creating their own widgets. Happy coding!