Writing for PyQt5 & PySide2

How to write code that works seamlessly with both PyQt5 and PySide2
Heads up! You've already completed this tutorial.

If you're building Python GUI applications with Qt, you've probably come across both PyQt5 and PySide2. These two libraries provide Python bindings to the same underlying Qt framework, and for the most part they are remarkably similar. But there are a handful of differences that can trip you up if you're not aware of them.

In this article, we'll look at what those differences are, and how you can write code that works with either library — whether you're building an application or distributing a reusable library.

Why two libraries?

PyQt5 and PySide2 both wrap the same C++ Qt library, so the widgets, layouts, signals, and other Qt concepts work the same way in both. The split comes down to licensing and history. PyQt5 is developed by Riverbank Computing and is available under the GPL or a commercial license. PySide2 is the official Qt for Python project, available under the LGPL.

For most developers, the practical differences between the two are small. The APIs are nearly identical. But "nearly" is the operative word — there are a few spots where they diverge.

The main differences

Let's walk through the areas where PyQt5 and PySide2 differ, and how to handle each one.

Signals and Slots

This is the most visible difference. In PySide2, custom signals and slots are defined using Signal and Slot from PySide2.QtCore. In PyQt5, the equivalents are called pyqtSignal and pyqtSlot.

PySide2:

python
from PySide2.QtCore import Signal, Slot

PyQt5:

python
from PyQt5.QtCore import pyqtSignal as Signal, pyqtSlot as Slot

By aliasing pyqtSignal to Signal and pyqtSlot to Slot at import time, you can use the same names throughout your code regardless of which library you're using.

uic and loading .ui files

If you use Qt Designer to build your interfaces, you'll work with .ui files. Both libraries can load these, but the mechanism is slightly different.

PyQt5 provides uic.loadUi() which can load a .ui file directly onto a widget:

python
from PyQt5 import uic

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        uic.loadUi("mainwindow.ui", self)

PySide2 uses QUiLoader, which creates and returns a new widget rather than loading onto an existing one:

python
from PySide2.QtUiTools import QUiLoader

loader = QUiLoader()
window = loader.load("mainwindow.ui")
window.show()

This difference is harder to paper over with a simple alias. If you need to support both, the cleanest approach is to compile your .ui files to Python code ahead of time using pyuic5 (PyQt5) or pyside2-uic (PySide2), and import the resulting module. The generated code will be specific to the library you compiled with, but your own code won't need to call any loader directly.

QApplication.exec() vs QApplication.exec_()

In Python 2, exec was a reserved keyword, so both libraries used exec_() as the method name. PySide2 now supports both exec() and exec_(). PyQt5 also supports both in recent versions. If you're using Python 3 (and you should be), prefer exec_() for maximum compatibility across library versions.

python
app.exec_()

Other minor differences

There are a few other small differences:

  • Property types: PyQt5 uses pyqtProperty, PySide2 uses Property.
  • qApp: Both provide a global application instance, but import locations can differ.
  • Enums: In some cases, the enum access patterns have minor variations, though this is more of a Qt version issue than a binding issue.

For most application code, signals/slots and .ui file loading are the only differences you'll encounter regularly.

Creating a compatibility layer

If you want your code to work with both libraries, a good approach is to create a small compatibility module — often called qt.py — that handles the import differences in one place. The rest of your code then imports from this module and never needs to worry about which binding is installed.

Here's a straightforward way to do this:

python
# qt.py
import os
import sys

# Determine which binding to use.
# Check for an environment variable first, then try importing.
QT_API = os.environ.get("QT_API", "").lower()

if QT_API == "pyqt5":
    from PyQt5 import QtGui, QtWidgets, QtCore
    from PyQt5.QtCore import pyqtSignal as Signal, pyqtSlot as Slot

elif QT_API == "pyside2":
    from PySide2 import QtGui, QtWidgets, QtCore
    from PySide2.QtCore import Signal, Slot

else:
    # No preference specified — try PyQt5 first, fall back to PySide2.
    try:
        from PyQt5 import QtGui, QtWidgets, QtCore
        from PyQt5.QtCore import pyqtSignal as Signal, pyqtSlot as Slot
    except ImportError:
        from PySide2 import QtGui, QtWidgets, QtCore
        from PySide2.QtCore import Signal, Slot

Then in the rest of your project, you import from qt instead of directly from PyQt5 or PySide2:

python
from qt import QtWidgets, Signal, Slot

Why the environment variable?

You might be wondering why we check QT_API instead of just using a try/except import. For applications, a simple try/except works well:

python
try:
    from PyQt5 import QtGui, QtWidgets, QtCore
    from PyQt5.QtCore import pyqtSignal as Signal, pyqtSlot as Slot
except ImportError:
    from PySide2 import QtGui, QtWidgets, QtCore
    from PySide2.QtCore import Signal, Slot

This is the "Easier to Ask Forgiveness than Permission" (EAFP) approach, and it's perfectly Pythonic. If PyQt5 is installed, it gets used. If not, PySide2 is tried instead.

But consider this scenario: you're writing a library that other developers will import into their applications. A developer is building their app with PySide2, but they also happen to have PyQt5 installed in the same environment (maybe for another project). With the try/except approach, your library would silently import PyQt5 — and now the application is mixing two different Qt bindings in the same process, which leads to crashes and deeply confusing bugs.

The environment variable approach lets the application developer declare which binding should be used, and your library respects that choice. Libraries like qtpy and Qt.py use this same pattern for exactly this reason.

For your own applications (where you control the environment), the try/except approach works perfectly. For libraries, the environment variable adds a layer of safety.

Using qtpy as an alternative

If you'd rather not maintain your own compatibility module, the qtpy package does all of this for you. It provides a unified API that wraps both PyQt5 and PySide2 (and PyQt6 and PySide6 too):

bash
pip install qtpy
python
from qtpy import QtWidgets
from qtpy.QtCore import Signal, Slot

qtpy checks the QT_API environment variable and automatically resolves differences between the bindings. It's used by the Spyder IDE and several other large projects, so it's well-tested.

A complete example

Here's a small application that uses our qt.py compatibility layer, so it will run on either PyQt5 or PySide2 — whichever you have installed:

qt.py:

python
import os

QT_API = os.environ.get("QT_API", "").lower()

if QT_API == "pyqt5":
    from PyQt5 import QtGui, QtWidgets, QtCore
    from PyQt5.QtCore import pyqtSignal as Signal, pyqtSlot as Slot

elif QT_API == "pyside2":
    from PySide2 import QtGui, QtWidgets, QtCore
    from PySide2.QtCore import Signal, Slot

else:
    try:
        from PyQt5 import QtGui, QtWidgets, QtCore
        from PyQt5.QtCore import pyqtSignal as Signal, pyqtSlot as Slot
    except ImportError:
        from PySide2 import QtGui, QtWidgets, QtCore
        from PySide2.QtCore import Signal, Slot

app.py:

python
import sys
from qt import QtWidgets, Signal, Slot, QtCore


class MainWindow(QtWidgets.QMainWindow):

    message_sent = Signal(str)

    def __init__(self):
        super().__init__()
        self.setWindowTitle("Cross-compatible Qt App")

        # Create widgets.
        self.input = QtWidgets.QLineEdit()
        self.input.setPlaceholderText("Type a message and press Enter...")
        self.output = QtWidgets.QLabel("No messages yet.")
        self.output.setAlignment(QtCore.Qt.AlignCenter)

        # Layout.
        layout = QtWidgets.QVBoxLayout()
        layout.addWidget(self.input)
        layout.addWidget(self.output)

        container = QtWidgets.QWidget()
        container.setLayout(layout)
        self.setCentralWidget(container)

        # Connections.
        self.input.returnPressed.connect(self.send_message)
        self.message_sent.connect(self.display_message)

    @Slot()
    def send_message(self):
        text = self.input.text()
        if text:
            self.message_sent.emit(text)
            self.input.clear()

    @Slot(str)
    def display_message(self, message):
        self.output.setText(f"You said: {message}")


def main():
    app = QtWidgets.QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec_())


if __name__ == "__main__":
    main()

Place both files in the same directory and run python app.py. The application will work with whichever Qt binding you have installed. If both are available, it will default to PyQt5 — or you can force a specific one by setting the environment variable:

bash
QT_API=pyside2 python app.py

Summary

PyQt5 and PySide2 are much more alike than they are different. The main points to remember are:

  • Signals/Slots are named differently (pyqtSignal/pyqtSlot vs Signal/Slot), but can be easily aliased.
  • .ui file loading works differently between the two libraries.
  • exec_() is the safe choice for compatibility.
  • A small compatibility module (qt.py) can centralize these differences so the rest of your code stays clean.
  • For applications, a try/except import works well. For libraries, use an environment variable to let the consuming application choose.
  • The qtpy package is a ready-made solution if you don't want to maintain your own shim.

With a little bit of setup, you can write Qt applications and libraries that work across both bindings without duplicating any code.

Well done, you've finished this tutorial! Mark As Complete
[[ user.completed.length ]] completed [[ user.streak+1 ]] day streak

Create GUI Applications with Python & Qt6 by Martin Fitzpatrick

(PySide6 Edition) The hands-on guide to making apps with Python — Over 15,000 copies sold!

More info Get the book

Martin Fitzpatrick

Writing for PyQt5 & PySide2 was written by Martin Fitzpatrick.

Martin Fitzpatrick has been developing Python/Qt apps for 8 years. Building desktop applications to make data-analysis tools more user-friendly, Python was the obvious choice. Starting with Tk, later moving to wxWidgets and finally adopting PyQt. Martin founded PythonGUIs to provide easy to follow GUI programming tutorials to the Python community. He has written a number of popular Python books on the subject.