PySide6 Is Here

Qt version 6.5 was released in April 2023, and now the Python API, PySide has been updated to map to the new Qt libraries. Try to install PySide2 using pip, and you’ll run into a road block. It’s been deprecated and the wheels are gone. Luckily, getting up and running fast with PySide6 is really straightforward.

Major Differences

Importing

Using PySide6 in your code is generally straightforward. You can simply replace the call to PySide2 with PySide6 in your import header.

Replace:

from PySide2.QtWidgets import QWidget

With…

from PySide6.QtWidgets import QWidget

QAction

This class has moved from QtWidgets to QtGui. Makes sense, because it’s not visibly presented.

from PySide6.QtQui import QAction

High DPI Scaling

One problem which could occur with PySide2 was the relative scaling of applications on devices with different DPI resolutions. This has been solved in PySide6 as high DPI is enabled by default. So you no longer have to engage boilerplate such as:

Qt.AA_EnableHighDpiScaling
Qt.AA_UseHighDpiPixmaps

QMouseEvent

To get the position of a mouse click, we now use the following code:

QMouseEvent.pos()  # deprecated: returned a tuple of ints
QMouseEvent.position()  # new: returns a QPointF object

QMouseEvent.globalPos()  # deprecated: return a tuple of ints
QMouseEvent.globalPosition()  # new: returns a QPointF object

# Qt.MidButton has been renamed to Qt.MiddleButton

QLayout

QLayout objects no longer have a setMargin() method.

Use setContentsMargins() instead supply either a QMargins() object or int for top, left, right, bottom parameters.

Snake Casing & Widget Properties

Bringing PySide inline with PEP8 standards, snake-casing has now been introduced in the method naming. But you have to import snake_case from the feature library. You can also set properties directly by invoking true_property.

from PySide6.QtWidgets import QWidget, QLabel, QVBoxLayout, QApplication
from __feature__ import snake_case, true_property


if __name__ == "__main__":
    app = QApplication()
    widget = QWidget()
    widget.set_layout(QVBoxLayout())
    label = QLabel()
    widget.layout().add_widget(label)
    label.text = "Hello"  # as opposed to label.setText("Hello")
    widget.show()
    app.exec()

Backwards Compatibility

One way to work with PySide6 is to address the classes using the base libraries. These are common to both PySide2 and PySide6 and you can import them using the following boiler-plate block:

try:
    from PySide6 import QtWidgets, QtGui, QtCore
except ImportError:
    from PySide2 import QtWidgets, QtGui, QtCore

Example Widget

So here’s an example widget just to show off some of the subtle differences in the style of code we can now use in PySide6 widgets.

from PySide6.QtCore import Signal
from typing import Sequence
from functools import partial
from __feature__ import true_property, snake_case

from core.enums import Alignment


class RadioButtonWidget(QWidget):
    clicked = Signal(int)

    def __init__(self, labels: Sequence[str], active_id: int = 0, alignment: Alignment = Alignment.vertical,
                 margin: int = 10, spacing: int = 2):
        """
        Self-contained widget featuring a set of radio buttons
        :param labels:
        :param active_id:
        :param alignment:
        :param margin:
        :param spacing:
        """
        super(RadioButtonWidget, self).__init__()
        self.window_title = self.__class__.__name__
        self.labels = labels
        self.radio_buttons = []
        layout: QLayout = QHBoxLayout() if alignment is Alignment.horizontal else QVBoxLayout()
        layout.contents_margins.fset = (margin, margin, margin, margin)
        layout.spacing = spacing
        self.set_layout(layout)
        button_group = QButtonGroup(self)

        for idx, button in enumerate(labels):
            button = QRadioButton(button)
            layout.add_widget(button)
            button_group.add_button(button)
            button.clicked.connect(partial(self.radio_button_clicked, idx))
            self.radio_buttons.append(button)

        self.radio_buttons[active_id].checked = True

    def radio_button_clicked(self, value):
        self.clicked.emit(value)

    @property
    def active_button(self) -> int:
        return next(i for i, value in enumerate(self.radio_buttons) if value.isChecked())

    @property
    def active_text(self) -> int:
        return self.labels[self.active_button]


if __name__ == '__main__':
    from PySide6.QtWidgets import QApplication
    app = QApplication()
    widget = RadioButtonWidget(['Tom', 'Dick', 'Harry'], alignment=Alignment.horizontal)
    widget.show()
    app.exec()

Things To Note

  • Margins: layout.contents_margins.fset = (margin, margin, margin, margin)
  • set_layout can’t currently be set using layout = layout
  • I used a custom enum to set the alignment.

Conclusions

My feeling about using true_property and snake_case so far is that it can be inconsistent. And once it is invoked, you are setting yourself a task to update each script when you are updating your widget library. There are specific Python environments which have yet to update their current working versions of PySide to PySide6, so updating the code using these methods make it hard to get things working. I might avoid that for a while, but I like the idea.

The deprecation of PySide2 means we tech artists now face a transition period of failing ui’s, just as we saw during the transition between PySide and PySide2. But I don’t see any major pains.

Addendum: Emergency PySide2

If you really need to install PySide2, while deprecated, the wheels are still available on the PySide2 website: https://pypi.org/project/PySide2/#files

  • Download a distribution.
  • You must be using Python 3.10 or prior, there is not a wheel for Python 3.11
  • Use pip install <distribution_path> to install the PySide2 module.

Leave a comment