Traits and TraitsUI: Reactive User Interfaces for Rapid Application Development in Python

The Enthought Tool Suite team is pleased to announce the release of Traits 4.6. Together with the release of TraitsUI 5.1 last year, these core packages of Enthought’s open-source rapid application development tools are now compatible with Python 3 as well as Python 2.7.  Long-time fans of Enthought’s open-source offerings will be happy to hear about the recent updates and modernization we’ve been working on, including the recent release of Mayavi 4.5 with Python 3 support, while newcomers to Python will be pleased that there is an easy way to get started with GUI programming which grows to allow you to build applications with sophisticated, interactive 2D and 3D visualizations.

A Brief Introduction to Traits and TraitsUI

Traits is a mature reactive programming library for Python that allows application code to respond to changes on Python objects, greatly simplifying the logic of an application.  TraitsUI is a tool for building desktop applications on top of the Qt or WxWidgets cross-platform GUI toolkits. Traits, together with TraitsUI, provides a programming model for Python that is similar in concept to modern and popular Javascript frameworks like React, Vue and Angular but targeting desktop applications rather than the browser.

Traits is also the core of Enthought’s open source 2D and 3D visualization libraries Chaco and Mayavi, drives the internal application logic of Enthought products like Canopy, Canopy Geoscience and Virtual Core, and Enthought’s consultants appreciate its the way it facilitates the rapid development of desktop applications for our consulting clients. It is also used by several open-source scientific software projects such as the HyperSpy multidimensional data analysis library and the pi3Diamond application for controlling diamond nitrogen-vacancy quantum physics experiments, and in commercial projects such as the PyRX Virtual Screening software for computational drug discovery.

 The open-source pi3Diamond application built with Traits, TraitsUI and Chaco by Swabian Instruments.

The open-source pi3Diamond application built with Traits, TraitsUI and Chaco by Swabian Instruments.

Traits is part of the Enthought Tool Suite of open source application development packages and is available to install through Enthought Canopy’s Package Manager (you can download Canopy here) or via Enthought’s new edm command line package and environment management tool. Running

edm install traits

at the command line will install Traits into your current environment.

Traits

The Traits library provides a new type of Python object which has an event stream associated with each attribute (or “trait”) of the object that tracks changes to the attribute.  This means that you can decouple your application model much more cleanly: rather than an object having to know all the work which might need to be done when it changes its state, instead other parts of the application register the pieces of work that each of them need when the state changes and Traits automatically takes care running that code.  This results in simpler, more modular and loosely-coupled code that is easier to develop and maintain.

Traits also provides optional data validation and initialization that dramatically reduces the amount of boilerplate code that you need to write to set up objects into a working state and ensure that the state remains valid.  This makes it more likely that your code is correct and does what you expect, resulting in fewer subtle bugs and more immediate and useful errors when things do go wrong.

When you consider all the things that Traits does, it would be reasonable to expect that it may have some impact on performance, but the heart of Traits is written in C and knows more about the structure of the data it is working with than general Python code. This means that it can make some optimizations that the Python interpreter can’t, the net result of which is that code written with Traits is often faster than equivalent pure Python code.

Example: A To-Do List in Traits

To be more concrete, let’s look at writing some code to model a to-do list.  For this, we are going to have a “to-do item” which represents one task and a “to-do list” which keeps track of all the tasks and which ones still need to be done.

Each “to-do item” should have a text description and a boolean flag which indicates whether or not it has been done.  In standard Python you might write this something like:

class ToDoItem(object):
def __init__(self, description='Something to do', completed=False):
self.description = description
self.completed = completed

But with Traits, this would look like:

from traits.api import Bool, HasTraits, Unicode
class ToDoItem(HasTraits):
description = Unicode('Something to do')
completed = Bool

You immediately notice that Traits is declarative – all we have to do is declare that the ToDoItem has attributes description and completed and Traits will set those up for us automatically with default values – no need to write an __init__ method unless you want to, and you can override the defaults by passing keyword arguments to the constructor:

>>> to_do = ToDoItem(description='Something else to do')
>>> print(to_do.description)
Something else to do
>>> print(to_do.completed)
False

Not only is this code simpler, but we’ve declared that the description attribute’s type is Unicode and the completed attribute’s type is Bool, which means that Traits will validate the type of new values set to these Traits:

>>> to_do.completed = 'yes'
TraitError: The 'completed' trait of a ToDoItem instance must be a boolean,
but a value of 'yes' <type 'str'> was specified.

Moving on to the second class, the “to-do list” which tracks which items are completed. With standard Python classes each ToDoItem would need to know the list which it belonged to and have a special method that handles changing the completed state, which at its simplest might look something like:

class ToDoItem(object):
def __init__(self, to_do_list, description='', completed=False):
self.to_do_list = to_do_list
self.description = description
self.completed = completed

def update_completed(self, completed):
self.completed = completed
self.to_do_list.update()

And this would be even more complex if an item might be a member of multiple “to do list” instances. Or worse, some other class which doesn’t have an update() method, but still needs to know when a task has been completed.

Traits solves this problem by having each attribute being reactive: there is an associated stream of change events that interested code can subscribe to. You can use the on_trait_change method to hook up a function that reacts to changes:

>>>> def observer(new_value):
...     print("Value changed to: {}".format(new_value))
...
>>> to_do.on_trait_change(observer, 'completed')
>>> to_do.completed = True
Value changed to: True
>>> to_do.completed = False
Value changed to: False

It would be easy to have the “to-do list” class setup update observers for each of its items. But, setting up these listeners manually for everything that you want to listen to can get tedious. For example, we’d need to track when we add new items and remove old items so we could add and remove listeners as appropriate.  Traits has a couple of mechanisms to automatically observe the streams of changes and avoid that sort of bookkeeping code. A class holding a list of our ToDoItems which automatically reacts to changes both in the list, and the completed state of each of these items might look something like this:

from traits.api import HasTraits, Instance, List, Property, on_trait_change
class ToDoList(HasTraits):
items = List(Instance(ToDoItem))
remaining_items = List(Instance(ToDoItem))
remaining = Property(Int, depends_on='remaining_items')

@on_trait_change('items.completed')
def update(self):
self.remaining_items = [item for item in self.items
if not item.completed]

def _get_remaining(self):
return len(self.remaining_items)

The @on_trait_change decorator sets up an observer on the items list and the completed attribute of each of the objects in the list which calls the method whenever a change occurs, updating the value of the remaining_items list.

An alternative way of reacting is to have a Property, which is similar to a regular Python property, but which is lazily recomputed as needed when a dependency changes.  In this case the remaining property listens for when the remaining_items list changes and will be recomputed by the specially-named _get_remaining method when the value is next asked for.

>>> todo_list = ToDoList(items=[
...     ToDoItem(description='Unify relativity and quantum mechanics'),
...     ToDoItem(description='Prove Riemann Hypothesis')])
...
>>> print(todo_list.remaining)
2
>>> todo_list.items[0].completed = True
>>> print(todo_list.remaining)
1

Perhaps the most important fact about this is that we didn’t need to modify our original ToDoItem in any way to support the ToDoList functionality.  In fact we can have multiple ToDoLists sharing ToDoItems, or even have other objects which listen for changes to the ToDoItems, and everything still works with no further modifications. Each class can focus on what it needs to do without worrying about the internals of the other classes.

Hopefully you can see how Traits allows you to do more with less code, making your applications and libraries simpler, more robust and flexible.  Traits has many more features than we can show in a simple example like this, but comprehensive documentation is available at http://docs.enthought.com/traits and, being BSD-licensed open-source, the Traits code is available at https://github.com/enthought/traits.

TraitsUI

One place where reactive frameworks really shine is in building user interfaces. When a user interacts with a GUI they change the state of the UI and a reactive system can use those changes to update the state of the business model appropriately. In fact all of the reactive Javascript frameworks mentioned earlier in the article come with integrated UI systems that make it very easy to describe a UI view declaratively with HTML and hook it up to a model in Javascript.  In the same way Traits comes with strong integration with TraitsUI, a desktop GUI-building library that allows you describe a UI view declaratively with Python and hook it up to a Traits model.

TraitsUI itself sits on top of either the wxPython, PyQt or PySide GUI library wrappers, and in principle could have other backends written for it if needed. Between the facilities that the Traits and TraitsUI libraries provide, it is possible to quickly build desktop applications with clear separation of concerns between UI and business logic.

TraitsUI uses the standard Model-View-Controller or Model-View-ViewModel patterns for building GUI applications, and it allows you to add complexity as needed. Often all that you require is a model class written in Traits and simple declarative view on that class, and TraitsUI will handle the rest for you.

Example: A To-Do List UI

Getting started with TraitsUI is simple. If you have TraitsUI and a compatible GUI toolkit installed in your working environment, such as by running the command-line:

edm install pyqt traitsui

then any Traits object has a default GUI available with no additional work:

>>> todo_item.configure_traits()

traits-properties

With a little more finesse we can improve the view. In TraitsUI you do this by creating a View for your HasTraits class:

from traitsui.api import HGroup, Item, VGroup, View
todo_item_view = View(
VGroup(
Item('description', style='custom', show_label=False),
HGroup(Item('completed')),
),
title='To Do',
width=360, height=240,
resizable=True,
)

Views are defined declaratively, and are independent of the model: we can have multiple Views for the same model class, or even have a View which works with several different model classes. You can even declare a default view as part of your class definition if you want. In any case, once you have a view you can use it by passing it as the view parameter:

>>>todo_item.configure_traits(view=todo_item_view)

This produces a fairly nice, if basic, UI to edit an object.

traits-todo

If you run these examples within an interactive IPython terminal session (such as from the Canopy editor, or the IPython QtConsole), you’ll see that these user interfaces are hooked up “live” to the underlying Traits objects: when you type into the text field or toggle the “completed” checkbox, the values of attributes change automatically. Coupled with the ability to write Traits code that reacts to those changes you can write powerful applications with comparatively little code. For a complete example of a TraitsUI application, have a look at the full to-do list application on Github.

These examples only scratch the surface of what TraitsUI is capable of. With more work you can create UIs with complex views including tables and trees, or add menu bars and toolbars which can drive the application. And 2D and 3D plotting is available via the Chaco and Mayavi libraries. Full documentation for TraitsUI is available at http://docs.enthought.com/traitsui including a complete example of writing an image capture application using TraitsUI.

Enthought Tool Suite

The Enthought Tool Suite is a battle-tested rapid application development system. If you need to make your science or business code more accessible, it provides a toolkit that you can use to build applications for your users with a lot less difficulty than using low-level wxPython or Qt code.  And if you want to focus on what you do best, Enthought Consulting can help with scientist-developers to work on your project.

But best of all the Enthought Tool Suite is open source and licensed with the same BSD-style license as Python itself, so it is free to use, and if you love it and find you want to improve it, we welcome your participation and contributions! Join us at http://www.github.com/enthought.

Traits and TraitsUI Resources

This entry was posted in Enthought Tool Suite, Open Source, Python, Traits and tagged on by .
avatar

About Corran Webster

Corran is Enthought's European consulting director, and has been with Enthought as a scientific software developer and Python instructor since 2008. Corran has a B.S. from the University of New South Wales and a Ph.D. in pure mathematics from UCLA and has held positions at the University of Nevada, Las Vegas and Texas A&M University, working primarily in operator algebras and functional analysis. Prior to joining Enthought, he was Chief Scientist at Compudigm International working on machine learning with self-organizing maps and large data visualization methods. Corran wrote his first Python code back in the days of Python 1.3 and has used it as his primary programming language for over 20 years.

Leave a Reply

Your email address will not be published. Required fields are marked *

Please leave these two fields as-is:

Protected by Invisible Defender. Showed 403 to 103,148 bad guys.