PySide is a Python binding for Qt. PySide is similar to the PyQt system but provides LGPL licensing and a community development model that makes it more attractive for commercial projects than PyQt. While the commercial license cost of PyQt is a factor the bigger issue with PyQt is the need to build it manually on each development system for every release: unlike the GPL version no binary installers are provided for the commercial PyQt.
We have been waiting for the right time to attempt migrating some projects from PyQt to PySide. The main issues were the maturity/stability of PySide and support for our full toolchain. We had experimented with early PySide releases with some applications and found that, despite a few problems and the lack of matplotlib plotting support, the GUIs ran correctly. With the recent release of PySide 1.1.0 and matplotlib 1.1.0 with PySide support it seemed that the time had come to attempt the full migration.
Recently we successfully ported a couple of engineering applications from PyQt to PySide. Here are some things we learned along the way.
Basics
The basics of a PyQt to PySide port can be found in the Differences Between PySide and PyQt page. Among other items this includes:
- Changing PyQt4 to PySide in your import statements.
- Changing pyqtSignal to Signal and pyqtSlot to Slot.
-
Removing QVariant and QString imports and replacing their use by the corresponding Python types.
Note that occurrences of QVariant() would normally be replaced by None but that may something to look at more carefully.
You also need to remove uses of toString() and toPyObject() since those are QVariant methods.
If the variable that had .toString() is not an str after the removal of QVariant wrappers then wrap it in str().
Removing QVariant wrappers could leave you with some lines of the form
var = var
or the more amusingvar = var if var is not None else None
and those can, of course, be removed. - Adding a [0] to the return value of the QFileDialog static function getOpenFileName to get the file name from the returned tuple. With PySide 1.1.0 you also need to do this for getOpenFileNames and getSaveFileName as well. Note that these return unicode strings so wrap the result in an str() call if a Python string is needed (and your app doesn't attempt to support Unicode path/file names).
Here are some other porting tips we learned along the way:
- PySide does not declare or call __del__() methods on its Qt derived classes when they are destroyed. In the rare case that you depended on PyQt calling your custom destructor you will have to call this yourself with PySide.
- Sometimes the object held in a QVariant might have been a Python builtin type or a user-defined type, requiring some code to handle it properly: you might have needed to test whether to call toPyObject() on it with PyQt. When QVariant is eliminated the need for such logic disappears.
-
PySide 1.1.0 has a bug in the QAction setShortcut() method: it won't accept a single shortcut value, either as a scalar or in a list with one item.
The work around is to wrap these scalar shortcuts like this:
action.setShortcut( QKeySequence( shortcut ) )
Packaging
Packaging with PyInstaller was expected to be an easy migration from PyQt but we hit a few speedbumps. The biggest was that PyInstaller sees matplotlib importing both PySide and PyQt modules and so it will bundle libraries from both of them if both are installed. If you have a commercial application but have the GPL PyQt installed (for non-commercial use, of course) this could create at least the appearance of a licensing issue. It took some trial and error to get the right solution because the PyInstaller documentation is a bit hand-wavy and because you have to look at the binaries and pure return values from Analysis() to see what to exclude. Adding the right excludes argument to the Analysis() constructor in the spec file does the trick:
a = Analysis( ..., excludes=['sip', 'PyQt4', 'PyQt4.QtCore', 'PyQt4.QtGui'])
If you simply remove PyQt dependencies from a.binaries after Analysis() is built you will still have the PyQt dependencies in your binaries and they will fail to run without the PyQt libraries.
Using matplotlib's pylab/pyplot interface causes PyInstaller to build a dependency on Tk into your application executables even though Tk is not used in a PySide application. Fixing this to avoid shipping Tk libraries with your application means growing our excludes call. On Windows it looks like this:
a = Analysis( ..., excludes=['sip', 'PyQt4', 'PyQt4.QtCore', 'PyQt4.QtGui', '_tkinter', 'tk85.dll', 'tcl85.dll'])
Since you are presumably using matplotlib's Qt4Agg backend it is nice to also eliminate the other backends. We find that you can just delete them from the binary collection PyInstaller generates but why not exclude them from the beginning and save the unnecessary copying/deletion:
a = Analysis( ..., excludes=['sip', 'PyQt4', 'PyQt4.QtCore', 'PyQt4.QtGui', '_tkinter', 'tk85.dll', 'tcl85.dll', 'matplotlib.backends._tkagg', 'matplotlib.backends._gtkagg', 'matplotlib.backends._backend_gdk'])
You may also need to modify the matplotlibrc and matplotlib.conf files PyInstaller puts in the mpl-data subdirectory to change the default backends to Qt4Agg. If your system-wide versions of those files already have this change then you won't need to do it as part of the packaging process.
Another issue we found was that the generated executables don't freeze in the QT_API=pyside environment choice set during the build phase so that your application will still need QT_API to be set at runtime. To avoid making users need to know/set this it is best to move the QT_API setup inside your code before the first matplotlib import that could happen in any code path:
os.environ[ 'QT_API' ] = 'pyside' import matplotlib