Python is well suited for rapid development of cross-platform applications of all sorts, and that includes desktop GUI apps. There are choices to be made when starting to develop a GUI application in Python, however, and this article provides the information you need to set out on the right path. We will discuss what serious GUI toolkits are available for Python, their strengths and weaknesses, and provide a simple demo application for each one.
Demo “hello world” application overview
The demo application is simple: a window containing a combo box that presents the choices “hello”, “goodbye”, and “heyo”; a text box in which free text can be entered; and a button which, when clicked, prints a greeting assembled from the values of the combo box and text box — e.g. “Hello, world!” — to the console. Here is a mockup of the example app:
Note that all the toolkits discussed here lend themselves to a similar object-oriented approach to our simple application; it is an object that inherits from a top-level GUI object, such as an application, window, or frame. The application window is filled with a vertical layout widget that manages the arrangement of the control widgets. Other GUI toolkits for Python, such as the long-obsolete pyFLTK or the simplistic easygui, take different approaches, but the toolkits discussed here represent the mainstream of Python GUI library development.
I have attempted to provide idiomatic code for each toolkit, but I am not expert in a couple of them; please let me know in the comments if I’ve done anything that might be misleading to new developers, and I’ll do my best to update the examples.
- Intro to PySide/PyQt: Basic Widgets and Hello, World!
- Review of IPython (alternative Python shell)
- Python’s Django vs Ruby on Rails
Choosing a Python GUI Toolkit
There are several toolkits available for GUI programming in Python; several are good, none are perfect, and they are not equivalent; there are situations where one or more otherwise excellent toolkits will not be appropriate. But the choice is not difficult, and for many applications, any GUI toolkit you choose will work. In the toolkit sections below, I’ve tried to honestly lay out the drawbacks and advantages of each, to help you make an informed choice.
A caution: Much of the information available online — for example, in the Python wiki’s GUI article or the Python 2.7 FAQ — is out-of-date and misleading. Many of the toolkits on those pages have been unmaintained for five years or more, and others are too immature or undocumented for serious use. Also, do not use the Choose Your GUI Toolkit page I’ve seen bandied about here and there; its weighting code makes it almost impossible to choose anything but wxPython.
We will look at the four most popular Python GUI toolkits: TkInter, wxPython, pyGTK/PyGobject, and PyQt/PySide.
TkInter
TkInter is the grand old man of the Python GUI world, having been distributed with Python almost since the language was founded. It is cross-platform and omnipresent, stable and reliable, and easy to learn, it has an object-oriented, brilliantly Pythonic API, and it works with all Python versions, but it has some drawbacks as well:
Limited support for theming
Before Python 2.7/3.1, TkInter had no support for theming, so on every platform its custom-drawn widgets looked like Motif circa 1985. Since, the ttk module has introduced themed widgets, which improve the look somewhat but leave some unorthodox controls, such as OptionMenus, in place.
Small selection of widgets
Tkinter lacks some widgets other toolkits provide, such as non-textual listboxes, real combo-boxes, scrolled windows, and others. Some additional controls have been supplied by third parties, e.g. Tix to supply the lack. Mock up your application in advance if you consider using TkInter, and make sure it can provide every control you need, or you could get burned later.
Demo App/Hello World in Tkinter
import Tkinter as tk class ExampleApp(tk.Frame): ''' An example application for TkInter. Instantiate and call the run method to run. ''' def __init__(self, master): # Initialize window using the parent's constructor tk.Frame.__init__(self, master, width=300, height=200) # Set the title self.master.title('TkInter Example') # This allows the size specification to take effect self.pack_propagate(0) # We'll use the flexible pack layout manager self.pack() # The greeting selector # Use a StringVar to access the selector's value self.greeting_var = tk.StringVar() self.greeting = tk.OptionMenu(self, self.greeting_var, 'hello', 'goodbye', 'heyo') self.greeting_var.set('hello') # The recipient text entry control and its StringVar self.recipient_var = tk.StringVar() self.recipient = tk.Entry(self, textvariable=self.recipient_var) self.recipient_var.set('world') # The go button self.go_button = tk.Button(self, text='Go', command=self.print_out) # Put the controls on the form self.go_button.pack(fill=tk.X, side=tk.BOTTOM) self.greeting.pack(fill=tk.X, side=tk.TOP) self.recipient.pack(fill=tk.X, side=tk.TOP) def print_out(self): ''' Print a greeting constructed from the selections made by the user. ''' print('%s, %s!' % (self.greeting_var.get().title(), self.recipient_var.get())) def run(self): ''' Run the app ''' self.mainloop() app = ExampleApp(tk.Tk()) app.run()
wxPython
Note that at the time of writing, wxPython is only available for Python 2.x
Guido van Rossum, the creator of Python, says of wxPython:
wxPython is the best and most mature cross-platform GUI toolkit, given a number of constraints. The only reason wxPython isn’t the standard Python GUI toolkit is that Tkinter was there first.
At least, he said that once, a long time ago; and how much GUI programming does Guido van Rossum do, anyway?
At any rate, wxPython does have a lot of good points. It has a vast set of widgets, native look-and-feel on Windows, Linux, and OS X, and a large-enough user base that at least the obvious bugs have been caught. It’s not included with Python, but installation is easy, and it’s available for all recent Python 2.x releases; wxPython is probably now the most popular GUI library for Python.
It’s not perfect, though; its documentation is atrocious, and though the exemplary demo app is priceless documentation itself, it’s about all the help you’re going to get. I personally find the API unPythonic and unpleasant, but others disagree. Still, you’ll almost never get in the middle of a project using wxPython and find it can’t do what you need, which is a big recommendation for it.
Demo App/Hello World in wxPython
Python 2.x
import wx class ExampleApp(wx.Frame): def __init__(self): # Every wx app must create one App object # before it does anything else using wx. self.app = wx.App() # Set up the main window wx.Frame.__init__(self, parent=None, title='wxPython Example', size=(300, 200)) # The greetings available self.greetings = ['hello', 'goodbye', 'heyo'] # Layout panel and hbox self.panel = wx.Panel(self, size=(300, 200)) self.box = wx.BoxSizer(wx.VERTICAL) # Greeting combobox self.greeting = wx.ComboBox(parent=self.panel, value='hello', size=(280, -1), choices=self.greetings) # Add the greeting combo to the hbox self.box.Add(self.greeting, 0, wx.TOP) self.box.Add((-1, 10)) # Recipient entry self.recipient = wx.TextCtrl(parent=self.panel, size=(280, -1), value='world') # Add the greeting combo to the hbox self.box.Add(self.recipient, 0, wx.TOP) # Add padding to lower the button position self.box.Add((-1, 100)) # The go button self.go_button = wx.Button(self.panel, 10, '&Go') # Bind an event for the button self.Bind(wx.EVT_BUTTON, self.print_result, self.go_button) # Make the button the default action for the form self.go_button.SetDefault() # Add the button to the hbox self.box.Add(self.go_button, 0, flag=wx.ALIGN_RIGHT|wx.BOTTOM) # Tell the panel to use the hbox self.panel.SetSizer(self.box) def print_result(self, *args): ''' Print a greeting constructed from the selections made by the user. ''' print('%s, %s!' % (self.greeting.GetValue().title(), self.recipient.GetValue())) def run(self): ''' Run the app ''' self.Show() self.app.MainLoop() # Instantiate and run app = ExampleApp() app.run()
pyGTK/pyGobject
pyGTK is a cross-platform widget toolkit based on GTK+, a broadly-used GUI toolkit developed originally for the image manipulation program GIMP. pyGobject is a new revision of pyGTK that uses GObject introspection and supports features of GTK3. Both versions’ APIs are closely related, and their capabilities and drawbacks are similar, so they can be considered as one for our purposes. The developers of pyGTK recommend using pyGobject for new development.
pyGTK only uses native widgets on Windows and Linux. Support for native widgets and windowing on OS X, though available in GTK+, is still under development in pyGTK; right now its look and feel on Mac emulates that of Linux. The widgets are, however, themeable, and there are a large number of very attractive themes. pyGTK is also most successful with Python 2.x; new versions begin to support Python 3 but are not complete, especially on Windows. Installation for Windows previously required downloading multiple packages from different sources, but recently all-in-one binary installers have been made available.
On the positive side, pyGTK is stable and well-tested and has a full selection of widgets. It also has excellent documentation and, if you like that sort of thing, an excellent visual GUI generator called Glade. Its API is straightforward and mostly simple, with only a few exceptions, and there is fairly good example code available. The bottom line for pyGTK: it is a solid and capable GUI toolkit that suffers from cross-platform inconsistency and the effects of being less popular than other equally good alternatives.
A special note about choosing pyGTK: if your application involves a variety of simple, text-based listboxes, pyGTK’s design will cause problems. Its strict separation of concerns and model-view-controller division make listboxes unusually complicated to develop compared to other toolkits. Failing to observe this point put me way behind on an important project when I was new to Python GUI development.
Demo App/Hello World in pyGTK/pyGobject
import gtk class ExampleApp(gtk.Window): ''' An example application for pyGTK. Instantiate and call the run method to run. ''' def __init__(self): # Initialize window gtk.Window.__init__(self) self.set_title('pyGTK Example') self.set_size_request(300, 200) self.connect('destroy', gtk.main_quit) # Structure the layout vertically self.vbox = gtk.VBox() # The greeting selector --- note the use of the convenience # type ComboBoxText if available, otherwise convenience # function combo_box_new_text, which is deprecated if (gtk.gtk_version[1] > 24 or (gtk.gtk_version[1] == 24 and gtk.gtk_version[2] > 10)): self.greeting = gtk.ComboBoxText() else: self.greeting = gtk.combo_box_new_text() # fix method name to match gtk.ComboBoxText self.greeting.append = self.greeting.append_text # Add greetings map(self.greeting.append, ['hello', 'goodbye', 'heyo']) # The recipient text entry control self.recipient = gtk.Entry() self.recipient.set_text('world') # The go button self.go_button = gtk.Button('_Go') # Connect the go button's callback self.go_button.connect('clicked', self.print_out) # Put the controls in the vertical layout self.vbox.pack_start(self.greeting, False) self.vbox.pack_start(self.recipient, False) self.vbox.pack_end(self.go_button, False) # Add the vertical layout to the main window self.add(self.vbox) def print_out(self, *args): ''' Print a greeting constructed from the selections made by the user. ''' print('%s, %s!' % (self.greeting.get_active_text().title(), self.recipient.get_text())) def run(self): ''' Run the app. ''' self.show_all() gtk.main() app = ExampleApp() app.run()
PyQt/PySide
Qt is more than a widget toolkit; it is a cross-platform application framework. PyQt, its Python interface, has been around for years, and is stable and mature; it has gained some cruft over the years, with two APIs available, known as API 1 and API 2, and a large number of deprecated features. Furthermore, even though Qt is available under the LGPL, PyQt is licensed under the GNU GPL versions 2 and 3 or in a fairly expensive commercial version, limiting your licensing options for your code.
PySide is a response to the drawbacks of PyQt. It is released under the LGPL and omits all deprecated features prior to PyQt 4.5 as well as the entire API 1, but otherwise is almost fully compatible with PyQt API 2. It is slightly less mature than PyQt, but is more actively developed.
No matter which wrapper you choose, Python and Qt work beautifully together. The more modern API 2 is fairly Pythonic and is clear, there is very good documentation available (although its roots in the C++ Qt documents are obvious), and the resulting applications look smashing in recent versions, if not quite native. There is every GUI widget you could possibly desire, and Qt supplies more — interesting classes to deal with XML, multimedia, database integration, and networking — though you’re probably better off using the equivalent Python libraries for most of the extra functionality.
The biggest downside of using a Qt wrapper is that the underlying Qt framework is huge. If it’s important to you that your distributable package be small, choose TkInter, which is not only small, but already a part of Python. Also, while Qt employs operating system APIs to adapt its widgets to your environment, not all are strictly native; wxPython would be a better choice if native look-and-feel is a major consideration for you. But PyQt has a lot going for it, and is certainly worth consideration.
Demo App/Hello World in PyQt/PySide
Note: For Qt, I’ve used PySide; the example code would run using PyQt with only a few lines’ difference.
import sys from PySide.QtCore import * from PySide.QtGui import * class ExampleApp(QDialog): ''' An example application for PyQt. Instantiate and call the run method to run. ''' def __init__(self): # create a Qt application --- every PyQt app needs one self.qt_app = QApplication(sys.argv) # The available greetings self.greetings = ['hello', 'goodbye', 'heyo'] # Call the parent constructor on the current object QDialog.__init__(self, None) # Set up the window self.setWindowTitle('PyQt Example') self.setMinimumSize(300, 200) # Add a vertical layout self.vbox = QVBoxLayout() # The greeting combo box self.greeting = QComboBox(self) # Add the greetings list(map(self.greeting.addItem, self.greetings)) # The recipient textbox self.recipient = QLineEdit('world', self) # The Go button self.go_button = QPushButton('&Go') # Connect the Go button to its callback self.go_button.clicked.connect(self.print_out) # Add the controls to the vertical layout self.vbox.addWidget(self.greeting) self.vbox.addWidget(self.recipient) # A very stretchy spacer to force the button to the bottom self.vbox.addStretch(100) self.vbox.addWidget(self.go_button) # Use the vertical layout for the current window self.setLayout(self.vbox) def print_out(self): ''' Print a greeting constructed from the selections made by the user. ''' print('%s, %s!' % (self.greetings[self.greeting.currentIndex()].title(), self.recipient.displayText())) def run(self): ''' Run the app and show the main form. ''' self.show() self.qt_app.exec_() app = ExampleApp() app.run()