What I Learned at Work this Week: Python GUIs with ipywidgets
Most of my enrichment time at work this week was spent wrestling with node_modules while trying to solve my Babel problem from last week. Rather than rehash that, I’m happy to report that I finally finished the Udemy Python course that I started in December. I was hoping that the last section would provide something I could blog about and it didn’t disappoint. Today we’ll be talking about Graphical User Interfaces (GUIs) in Python.
Graphical User Interface
I think I started hearing people use the phrase GUI (they pronounced it “goo-ey”) about a month into my code-learning journey. At that point I was used to hearing people talk about things I didn’t understand, so I ignored them and dutifully pressed forward building my simple Ruby on Rails app. If I had taken the time to investigate, I would have learned that Graphical User Interface is just a fancy way of describing most computer programs we interact with every day. If you’re filling out a form or clicking a button, you’re using a GUI. These types of elements can be created using HTML, styled with CSS, and given additional interactivity with JavaScript. So naturally my first thought when seeing them come up in a Python course was…how?
ipywidgets
The Udemy lesson focused on a Python library called ipywidgets. Through a seamless integration with Jupyter Notebooks, this library can build a GUI without asking the programmer to write any traditional front-end syntax. I wasn’t familiar with Jupyter Notebooks before starting the course, but I found them easy to install and very easy to use. They are essentially an interface for running Python code that, unlike Repl or the command line, will play nicely with front-end elements. If you’d like to follow along with me or try out your own Python GUIs, you can check out Jupyter Notebooks here.
Widgets in Jupyter
We’ve established that ipywidgets is a library, so if we want to use it, we’re going to start with an import statement:
What can ipywidgets do? Jupyter Notebooks lets us take a look into some options within the library if we invoke our import, add a period, and press tab:
We might first notice that all of our options here are capitalized. I was originally thinking of widgets as an object of its own with methods, but it’s important to remember that it’s referencing an entire library and what we’re seeing are options for class instantiation. I’d like to start testing things out with a slider, so I’ll search for the IntSlider class:
This is what I mean when I say seamless integration and GUI without front-end syntax. The library also utilizes Bootstrap to provide customization options. Say I wanted my slider to be vertical instead of horizontal:
There’s a ton of different options and different widgets, so if you’re familiar with Bootstrap, you can use some of those iconic button colors (I still haven’t figured out how to match the font though):
This is so cool! I wonder…can we figure out how it works?
Github
We can check out the documentation for ipywidgets here. When we click through, we see a little Github link in the upper-right hand corner of the page. Clicking on that will take us to the repo for this library — we can read all the code from there! A lot of popular projects and frameworks will grant us similar access; it’s ideal for when we want to know how something works.
I’m fairly confident that I could read JavaScript code that builds a button and gives it functionality, so I wonder if, having now completed my Python course, I can similarly understand how ipywidgets does it. From the top layer of the repo, I clicked around until I found the path that might be a good starting point: ipywidgets/ipywidgets/widgets/widget_button.py
At the very top of the file, we see a note:
"""Button class.Represents a button in the frontend using a widget. Allows user to listen forclick events on the button and trigger backend code when the clicks are fired."""
And if we scroll down a bit, we can see the Button class being defined:
Wow this is some good stuff. Shout out to the authors of this module for so fantastically documenting their work. Before we get there, though, we see the class definition with two parameters. Remember that in Python, these parameters indicate which superclasses our class inherits from. So a Button is both a DOMWidget and a CoreWidget. If we check out the code for DOMWidget, we see that it doesn’t give us much additional functionality, but does some heavy lifting styling-wise. It includes add_class and remove_class methods, which update our _dom_classes property. We can intuit that these classes, along with the contents of our layout property, are being used to style our new element. But the syntax we’re seeing could use some unpacking. We’ll just focus on _dom_classes for now:
_dom_classes = TypedTuple(trait=Unicode(), help="CSS classes applied to widget DOM element").tag(sync=True)
In the file, we can see the TypedTuple import at the top of the page, coming from .trait_types. Notice again that it’s capitalized, so it looks like _dom_classes is going to be assigned to an instance of a class. This class is instantiated with two arguments: trait and help. Next, we invoke a .tag method on the instance and pass a sync argument. Before doing any more investigation, I’m going to see if there’s anything about this I can understand. The help argument looks pretty straightforward — it’s just a string that, I would guess, is providing a description of what the property _dom_classes is doing. With that out of the way, we’ll start with our first function. Is there any way we can learn more about what Typed Tuple does?
Once again we’ve got a great description to help ease our understanding. Apparently a TypedTuple is A trait for a tuple of any length with type-checked elements. I learned from the traitlets documentation that it’s likely being a subclass of traitlets.Container that does the type-checking (“Traitlets is a framework that lets Python classes have attributes with type checking”). If you’re not native to Python, a tuple is a data collection that is ordered and immutable.
The docs also explain that the trait property is used to check the values provided to the data structure. In this case our data structure is a tuple and since we’re checking that the data type is Unicode, it would be fair to call it…a TypedTuple! Finally, the .tag method is used to add metadata to our traitlet. Going back to the ipywidget docs, “The sync=True keyword argument tells the widget framework to handle synchronizing that value to the browser. Without sync=True, attributes of the widget won’t be synchronized with the front-end.” That is to say that if our value changes on the back-end, we want that to be reflected on the front-end.
The Button
Okay so it’s starting to make sense how data and code flows throughout this library. We’re using Object Oriented Programming to initialize Widget subclasses like widgets.IntegerSlider or widget.Button. So what does the Button class actually entail, besides inheriting some styling options?
The first property we see defined is _view_name, which once again uses traitlets to make sure that it receives a Unicode value and that we’re taking advantage of ipywidgets’ sync=True option. The value passed in is the string ‘ButtonView’, which is responsible for actually indicating the type of widget that will be rendered. The Dropdown class uses the view name ‘DropdownView’, the RadioButtons class uses ‘RadioButtonsView’, and so on. Again, consulting the documentation:
Inheriting from the DOMWidget does not tell the widget framework what front end widget to associate with your back end widget. Instead, you must tell it yourself by defining specially named trait attributes, _view_name, _view_module, and _view_module_version (as seen below) and optionally _model_name and _model_module.
Like any powerful library, ipywidgets is huge, so we’ll look at one more method before wrapping up this week’s entry. Our Button class, of course, has an on_click method:
Its parameters are self (all Python methods take a first argument of self), callback (the function to be invoked when the button is clicked), and remove (with a default value of False). The optional third argument lets us remove our callback from the callback list, if we desire (I tried to do research on why we would want to do this, but couldn’t figure it out). But it’s a very straightforward method and the others associated with Button (click and _handle_button_msg) are similarly straightforward.
Models and Views
I kept poking around the codebase looking for some JavaScript. I wondered if it might actually be possible to build a responsive front-end with only Python. Finally, I found this paragraph on page 71 of this documentation.
The IPython widget framework front end relies heavily on Backbone.js. Backbone.js is an MVC (model view controller) framework. Widgets defined in the back end are automatically synchronized with generic Backbone.js models in the front end. The traitlets are added to the front end instance automatically on first state push. The _view_name trait that you defined earlier is used by the widget framework to create the corresponding Backbone.js view and link that view to the model.
Until now, I had been thinking a lot about how this library reminded me of React in that it used passed arguments to determine styling and used sync to build controlled components. After reading this, though, I was brought back to the first time I ever heard “GUI” — my Ruby on Rails project. After all this time, we’re working with an MVC framework once again. Our library is working with JS after all and as our wealth of knowledge grows, we continue to become better equipped to recognize these patterns and work through our roadblocks.