Over the past 5 years front-end developers seem to have arrived at the conclusion that declarative programming is usually bettern than imperative. This trend is largely evidenced by the rise of Javascript frameworks like Vue and React which describe the logic of computations without explicitly stating their control flow.
So what does this have to do with Python? Well, because browsers are the de facto "operating system of the internet", even back-end languages like Python have had to figure out clever ways to integrate with it. While standard REST APIs are well suited to applications built using HTML templates, they don't always enable the degree of interactivity expected by modern browser users. Additionally, the learning curve for Python beginners is steepened since knowing Python isn't enough on its own to build a full-stack application.
In the wake of these and other problems, Python-based packages like IPyWidgets, Streamlit and Plotly Dash have sprung up to solve them. However they have their own limitations:
-
IPyWidgets and Streamlit are imperative frameworks. So many of the lessons learned by developers of front-end frameworks have not been translated into their Python-based counterparts.
-
At their initial inception, most of these libraries were driven by the visualization needs of data scientists, so the ability to design arbitrary UI layouts remains stunted.
-
All are limited to specific ecosystems. For example IPyWidgets requires a back-end Jupyter Kernel in order to send data back and forth between Python and the front-end.
Enter IDOM
A new declarative Python package for building user interfaces.
IDOM takes obvious inspiration from React, and wherever possible, attempts to achieve parity with the features it copies more directly. Nowhere is this more evident than the version of React's often lauded "Hooks" that IDOM implements in Python.
At a glance, the similarities between IDOM and React are rather striking. Below is a React component which defines a simple Slideshow
displaying an image that updates when a user clicks on it, and immediately below it is the same view implemented in Python using IDOM:
import React, { useState } from react;
function Slideshow() {
const [index, setIndex] = useState(0);
return (
<img
src={ `https://picsum.photos/800/300?image=${index}` }
onClick={ () => setIndex(index + 1) }
style={ {cursor: "pointer"} }
/>
)
}
import idom
@idom.element
def Slideshow():
index, set_index = idom.hooks.use_state(0)
return idom.html.img(
{
"src": f"https://picsum.photos/800/300?image={index}",
"onClick": lambda event: set_index(index + 1),
"style": {"cursor": "pointer"},
}
)
idom.run(Slideshow)
Just so we can see what makes IDOM so unique among Python's UI packages, let's take a look at what it would take to recreate the same Slideshow
from the prior section using IPywidgets (the most popular of the three alternatives mentioned above):
from ipywidgets import widgets
from ipyevents import Event
from IPython.display import display
def slideshow():
data = {"index": 0}
img_widget = widgets.HTML()
update_slideshow_widget(img_widget, data)
on_click = Event(source=img_widget, watched_events=["click"])
def click_handler(*args, **kwargs):
img_widget.value = update_slideshow_widget(img_widget, data)
on_click.on_dom_event(click_handler)
return img_widget
def update_slideshow_widget(widget, data):
new_html = html_slideshow_img(data["index"])
widget.value = new_html
data["index"] += 1
def html_slideshow_img(index):
url = f"https://picsum.photos/800/300?image={index}"
return html_img(src=url, style={"cursor": "pointer"})
def html_img(src, style):
style_str = ";".join(f"{k}:{v}" for k, v in style.items())
return f"<img src={src!r} style={style_str!r} />"
display(slideshow())
There's several important differences to note about the IPyWidgets implementation versus the one done using IDOM...
from ipyevents import Event
IPyWidgets does not provide a built-in way to register event handlers to standard HTML elements. To do so requires implementing a custom widget the explanation of which would require its own lengthy blog post. Thus we need to use ipyevents
.
Normally, installing third party packages to achieve a specialized need is perfectly alright. However in this case I think it's hard to argue that being able to quickly and easily register DOM event handlers is a "specialized" need.
IDOM on the other hand is designed from the ground up with this sort of interactivity in mind. All you need to do is assign a function to an event name in the element's attribute dictionary:
"onClick": lambda event: set_index(index + 1)
widget.value = new_html
IPyWidgets has to update with UI with raw HTML (unless you have a custom widget) by setting the innerHTML
attribute of a DOM element (which is a particularly expensive operation). When running the code above, the image flickers in and out of existence whenever a new image is loaded:
IDOM computes the difference between the old and new layout state and only sends what what has changed. Those changes are then sent to a front-end React-based client implemented in React which also optimizes the number of DOM mutations required to evolve the view. These two things together solve the flickering problem seen above:
return f"<img src={src!r} style={style_str!r} />"
IPywidgets requires Python users to construct raw HTML strings when no existing widgets suite their needs. Perhaps if IPyWidgets were a comprehensive templating library (in the way Vue is) this could even be advantageous, but given this is not the case, attempting to programmatically constructing a UI out of strings is quite awkward.
IDOM uses dictionaries to create a "Virtual" representation of the Document Object Model (VDOM). This makes it incredibly easy to write straightforward and composable functions that return VDOM dictionaries. Further the use of standardized VDOM representation means creating third party IDOM client implementations is effortless (see Ecosystem Independence for more info)
While continuing to compare IDOM to other Python-based UI packages does highlight some of its capabilites, there's so much more it can do...
Even though IDOM is intended to put power into the hands of pure Python developers, the fact that Python is a back-end language means that there will always be some inherent limitations. As a result, you may need to use existing Javascript libraries. Thankfully though, this is dead simple in IDOM. If you're just experimenting use the idom
CLI to intall any Javascript you're looking for from NPM:
$ idom install victory
Then you can directly import it into IDOM and use it just like any other element:
If you wish to distribute your own custom Javascript-based components as a Python package. IDOM provides a repository template that can be set up using the popular cookiecutter project. This template represents the most up-to-date and recommended method of distributing Javascript for use by IDOM:
$ pip install cookiecutter
$ cookiecutter https://github.com/idom-team/idom-package-cookiecutter.git
You can target your usage of IDOM in your production-grade applications with IDOM's Javascript client library. Just install the client in your front-end app and connect it to a back-end websocket that's serving up IDOM models - instead of creating your whole application with IDOM, you can use it exactly where you need it. Further, the ability to leverage custom javascript in IDOM means that you can progressively develop React components for your particular use cases. In a fully matured application, IDOM can act primarilly as a simple, yet performant, data synchronization layer.
The core abstractions of IDOM have well defined interfaces and protocols that enforce clear boundaries of communication.
This means that you aren't locked into any particular layer of the stack. You can swap out the web-server or even the client with relative ease - IDOM's built-in implementations for both the web-server and client are collectively just a few hundred lines of code.
To emphasize the point of independence, look no further than IDOM's integration with IPyWidgets - idom-jupyter is a Python package that natively integrates IDOM into Juyter as a Jupyter Widget with a similarly small amount of Python and Javascript:
You can try it out right now in Binder!
For a fledgling project IDOM's documentation is surprisingly thorough. Check it out:
Building highly interactive web applications as a Python developer has historically been a great challenge. However, IDOM changes that. Knowing just basic HTML, CSS, and Python, you can make everything from slideshows to interactive dashboards. Its usage can be as broad or targeted as you need it to be, and you can use it wherever you need it, whether that's in a Jupyter notebook or standard web application.