Last time I have shown you the basics of PySide6 and what you can do with it. My colleague Berthold further demonstrated the use of Matplotlib to visualize data. It’s one of the the key selling points for using Python in your project. As with most things in life, where there is light, there is darkness. Let’s have a look at some of the pitfalls to look out for, particularly when piping lots of data through a PySide6 application.
In addition to a plot to visualize your data, you most likely also need some sort of list or table to choose what data to render in the first place. Qt comes with a powerful set of item view classes that use a model-view architecture to manage data and how it’s presented to the user.
Be careful with the Qt global object!
Let’s have a look at the following snippet from a table model returning some data (of course, somewhat simplified):
def data(self, index, role):
item = index.internalPointer()
match role:
case Qt.DisplayRole:
return item.text()
case Qt.DecorationRole:
return item.icon()
case Qt.ToolTipRole:
return item.toolTip()
Spot anything wrong with this? This method is called a lot, every entry in the model has a dozen of roles (display, decoration, tooltip, foreground color, etc). Now imagine scrolling through a table with 10 columns and maybe 30 entries per screen page. While Qt is clever enough not to ask for cells that are out of the visible area and only fetches data when actually needed (e.g. tooltip role is only queried when you hover the item), you’re still looking at maybe 100 calls for each scrolled line. Still, looks like your typical Qt item model code, doesn’t it?
While investigating a performance problem with a table model, we fired up a profiler and were dismayed by what we found: It wasn’t our custom delegate painting code after all! Instead, python spent a good amount of time in PyObject_GetAttr
. Turns out, the Qt
object is huge. It contains all the enum values you could think of. Calling Qt.DisplayRole
then has to look up the corresponding value in this giant object. Narrowing it down by using Qt.ItemDataRole.DisplayRole
significantly improved the model’s performance. Likewise, in painting code you probably want to use Qt.GlobalColor.transparent
instead of Qt.transparent
to fill your pixmap. While it doesn’t hurt much in a button handler, it definitely adds up in a function that is called thousands of times. Therefore: always use fully qualified enums with PySide6!
Function calls are expensive
lessThan
or filterAcceptsRow
in your QSortFilterProxyModel
or making use of __lt__
in your QListWidget
, they are also called frequently. If you can, try to use the built-in sort or filter criteria to keep the logic on the C++ side. In case of custom sorting, your model’s sort()
function is a good place to do this since it’s only called when actually changing the sort order. For example, rather than adjusting the sort role on the fly like this:
def lessThan(self, source_left, source_right):
if self.sortColumn() == MyModel.Columns.SPECIAL_COLUMN:
data_left = source_left.data(MyModel.Roles.SPECIAL_ROLE)
data_right = source_right.data(MyModel.Roles.SPECIAL_ROLE)
return data_left < data_right
else:
return super().lessThan(source_left, source_right)
Adjust the sort column just once:
def sort(self, column, order=Qt.SortOrder.AscendingOrder):
self.setSortRole(MyModel.Roles.SPECIAL_ROLE if column == MyModel.Columns.SPECIAL_COLUMN else Qt.ItemDataRole.DisplayRole)
super().sort(column, order)
Of course, this is only an option if the sorting algorithm is quite simple. It can still come in handy some day.
API caveats
Finally, there’s a few API concepts in Qt that don’t map well to the Python environment, notably, a function taking a reference and thereby altering an existing object. One such example is QValidator
. It has a fixup(QString &input)
method that you can override to sanitize the given input. PySide6 addresses this by altering the function signature to instead return a string. This is a common difference between C++ Qt and PySide6. Unfortunately, sometimes this wasn’t done consistently and the aforementioned method wasn’t usable from Python. Luckily, once reported, the bug was fixed within an hour and from Qt 6.8.3 it’s possible to write fully custom input validators in Python!
Another one is QDataStream
: In C++ you can stream stuff into it using operator<<
. In Python, while possible, it often doesn’t yield the desired result since Python really only knows Int and Float (and Complex) numbers. Instead, use the type-safe readInt16
, readInt32
, readUInt16
, readUInt32
, etc methods PySide6 adds.
Conclusion
PySide6 is easy to get started with, both when coming from a Python or C++ background. However, there are a few things to consider, particularly if you have a strong background with Qt in C++. If you merely write down the code you thought up in your head with a different syntax (i.e. without braces and semicolons) you can be in for a little surprise as we have seen above. The overhead of Python as an interpreted language and the translation between it and C++ can add up, too. Nevertheless, this blog post should help you overcome the worst obstacles in your next Qt project with Python!
- Kai Uwe Broulik
- //
- // Pyside, Python, Qt