Qt for MCUs 2.3 released

Since the very first release of Qt for MCUs, your feedback and requests have been driving the development of Qt for MCUs. Today, we are happy to announce the release of version 2.3, which includes several of the most requested features and improvements. This new version adds the Loader QML type to Qt Quick Ultralite, support for partial framebuffers to substantially reduce the overall memory requirements of your applications, support for building applications using MinGW on Windows, and much more!

QPushButton

The push button, or command button, is perhaps the most commonly used widget in any graphical user interface (GUI). A button is a rectangular widget that typically displays a text describing its aim.

When we click a button, we command the computer to perform actions or to answer a question. Typical buttons include Ok, Apply, Cancel, Close, Yes, No and Help. However, they're not restricted to this list.

In this tutorial, you'll learn how to create and customize button widgets in your GUI applications.

Creating Buttons Widgets With QPushButton

Buttons are probably the most common widgets in GUI applications. They come in handy when you need to create dialogs for communicating with your users. You'll probably be familiar with Accept, Ok, Canel, Yes, No, and Apply buttons, which are commonplace in modern graphical user interfaces.

In general, buttons allow you to trigger actions in response to the user's clicks on the button's area. Buttons often have a rectangular shape and include a text label that describes their intended action or command.

If you've used PyQt or PySide to create GUI applications in Python, then it'd be pretty likely that you already know about the QPushButton class. This class allows you to create buttons in your graphical interfaces quickly.

The QPushButton class provides three different constructors that you can use to create buttons according to your needs:

Constructor Description
QPushButton(parent: QWidget = None) Constructs a button with a parent widget but without text or icon
QPushButton(text: str, parent: QWidget = None) Constructs a button with a parent widget and the description text but without an icon
QPushButton(icon: QIcon, text: str, parent: QWidget = None) Constructs a button with an icon, text, and parent widget

To illustrate how to use the above constructors, you can code the following example:

python
import sys

from PyQt6.QtCore import QSize
from PyQt6.QtGui import QIcon
from PyQt6.QtWidgets import QApplication, QPushButton, QVBoxLayout, QWidget

class Window(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        # Button with a parent widget
        topBtn = QPushButton(parent=self)
        topBtn.setFixedSize(100, 60)
        # Button with a text and parent widget
        centerBtn = QPushButton(text="Center", parent=self)
        centerBtn.setFixedSize(100, 60)
        # Button with an icon, a text, and a parent widget
        bottomBtn = QPushButton(
            icon=QIcon("./icons/logo.svg"),
            text="Bottom",
            parent=self
        )
        bottomBtn.setFixedSize(100, 60)
        bottomBtn.setIconSize(QSize(40, 40))
        layout = QVBoxLayout()
        layout.addWidget(topBtn)
        layout.addWidget(centerBtn)
        layout.addWidget(bottomBtn)
        self.setLayout(layout)

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = Window()
    window.show()
    sys.exit(app.exec())

python
import sys

from PySide6.QtCore import QSize
from PySide6.QtGui import QIcon
from PySide6.QtWidgets import QApplication, QPushButton, QVBoxLayout, QWidget

class Window(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        # Button with a parent widget
        topBtn = QPushButton(parent=self)
        topBtn.setFixedSize(100, 60)
        # Button with a text and parent widget
        centerBtn = QPushButton(text="Center", parent=self)
        centerBtn.setFixedSize(100, 60)
        # Button with an icon, a text, and a parent widget
        bottomBtn = QPushButton(
            icon=QIcon("./icons/logo.svg"),
            text="Bottom",
            parent=self
        )
        bottomBtn.setFixedSize(100, 60)
        bottomBtn.setIconSize(QSize(40, 40))
        layout = QVBoxLayout()
        layout.addWidget(topBtn)
        layout.addWidget(centerBtn)
        layout.addWidget(bottomBtn)
        self.setLayout(layout)

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = Window()
    window.show()
    sys.exit(app.exec())

You can download the logo here or use your own image file. You can also use PNG format images if you prefer.

This code does a lot! First, we do the required imports. Inside our Window class, we create three QPushButton instances. To create the first button, we use the first constructor of QPushButton. That's why we only pass a parent widget.

For the second button, we use the second constructor of QPushButton. This time, we provide the button's text and parent. Note that the text is a regular Python string.

Our last button uses the third constructor. In this case, we need to provide the button's icon, text, and parent. We use the QIcon class with an SVG image as an argument to provide the icon. Note that we can also use a QPixmap object as the button's icon.

Save this code to a .py file and run it. You'll get a window that looks something like this:

QPushButton constructors example, showing three buttons with labels & icon QPushButton constructors example, showing three buttons with labels & icon

The first button has no text. It's just a rectangular shape on the app's windows. The second button in the center has text only, while the third button at the bottom of the window has an icon and text. That looks great!

These buttons don't do any action jet. If you click them, then you'll realize that nothing happens. To make your buttons perform concrete actions, you need to connect their signals to some useful slots. You'll learn how to do this in the next section.

Connecting Signals and Slots

Depending on specific user events, the QPushButton class can emit four different signals. Here's is a summary of these signals and their corresponding meaning:

Signal Emitted
clicked(checked=false) When the user clicks the button and activates it.
pressed() When the user presses the button down.
released() When the user releases the button.
toggled(checked=false) Whenever a checkable button changes its state from checked to unchecked and vice versa.

When you're creating a GUI application, and you need to use buttons, then you need to think of the appropriate signal to use. Most of the time, you'll use the button's clicked() signal since clicking a button is the most common user event you'll ever need to process.

The other part of the signal and slot equation is the slot itself. A slot is a method or function that performs a concrete action in your application. Now, how can you connect a signal to a slot so that the slot gets called when the signal gets emitted? Here's the required syntax to do this:

python
button.<signal>.connect(<method>)

In this construct, button is the QPushButton object we need to connect with a given slot. The <signal> placeholder can be any of the four abovementioned signals. Finally, <method> represents the target slot or method.

Let's write an example that puts this syntax into action. For this example, we'll connect the clicked signal with a method that counts the clicks on a button:

python
import sys

from PyQt6.QtWidgets import QApplication, QPushButton, QVBoxLayout, QWidget

class Window(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.count = 0
        self.button = QPushButton(f"Click Count: {self.count}", self)
        self.button.setFixedSize(120, 60)
        self.button.clicked.connect(self.count_clicks)
        layout = QVBoxLayout()
        layout.addWidget(self.button)
        self.setLayout(layout)

    def count_clicks(self):
        self.count += 1
        self.button.setText(f"Click Count: {self.count}")

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = Window()
    window.show()
    sys.exit(app.exec())

python
import sys

from PySide6.QtWidgets import QApplication, QPushButton, QVBoxLayout, QWidget

class Window(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.count = 0
        self.button = QPushButton(f"Click Count: {self.count}", self)
        self.button.setFixedSize(120, 60)
        self.button.clicked.connect(self.count_clicks)
        layout = QVBoxLayout()
        layout.addWidget(self.button)
        self.setLayout(layout)

    def count_clicks(self):
        self.count += 1
        self.button.setText(f"Click Count: {self.count}")

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = Window()
    window.show()
    sys.exit(app.exec())

The button variable holds an instance of QPushButton. To create this button, we use a text and parent widget. This parent widget works as our current window. Then we connect the button's clicked signal with the count_clicks() method.

The count_clicks() method counts the number of clicks on the button and updates the button's text accordingly. Go ahead and run the app!

Exploring the Public API of QPushButton

Up to this point, you've learned about the different ways to create buttons in your GUI applications. You've also learned how to make your buttons perform actions in response to the user's actions by connecting the buttons' signals with concrete methods known as slots.

In the following sections, you'll learn about the QPushButton class's public API and its most useful properties, including the following:

Property Description Getter Method Setter Method
text Holds the text shown on the button text() setText()
icon Holds the icon shown on the button icon() setIcon()
shortcut Holds the keyboard shortcut associated with the button shortcut() setShortcut()

Let's kick things off by learning how to set and get a button's text, icon, and keyboard shortcut. These actions can be an essential part of your GUI design process.

Setting a Button's Text, Icon, and Shortcut

In previous sections, you've learned how to create buttons using different class constructors. Some of these constructors allow you to set the button's text directly. However, sometimes you need to manipulate a button's text at runtime. To accomplish this, you can use setText() and text().

As its name suggests, the setText() method allows you to set the text of a given button. On the other hand, the text() lets you retrieve the current text of a button. Here's a toy example of how to use these methods:

python
import sys

from PyQt6.QtWidgets import QApplication, QPushButton, QVBoxLayout, QWidget

class Window(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.button = QPushButton("Hello!")
        self.button.setFixedSize(120, 60)
        self.button.clicked.connect(self.onClick)
        layout = QVBoxLayout()
        layout.addWidget(self.button)
        self.setLayout(layout)

    def onClick(self):
        text = self.button.text()
        if text == "Hello!":
            self.button.setText(text[:-1] + ", World!")
        else:
            self.button.setText("Hello!")

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = Window()
    window.show()
    sys.exit(app.exec())

python
import sys

from PySide6.QtWidgets import QApplication, QPushButton, QVBoxLayout, QWidget

class Window(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.button = QPushButton("Hello!")
        self.button.setFixedSize(120, 60)
        self.button.clicked.connect(self.onClick)
        layout = QVBoxLayout()
        layout.addWidget(self.button)
        self.setLayout(layout)

    def onClick(self):
        text = self.button.text()
        if text == "Hello!":
            self.button.setText(text[:-1] + ", World!")
        else:
            self.button.setText("Hello!")

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = Window()
    window.show()
    sys.exit(app.exec())

In this example, we use text() and setText() inside the onClick() method to manipulate the text of our button object. These methods come in handy when we need to set and retrieve a button's text at run time, which would be useful in a few situations. For example, if we need a button to fold and unfold a tree of widgets or other objects.

Go ahead and run the app! You'll get a window like the following:

Window with a single button, with the text Hello! Click to change the text. Window with a single button, with the text Hello! Click to change the text.

In this example, when you click the button, its text alternate between Hello! and Hello, World!.

QPushButton also has methods to manipulate the icon of a given button. In this case, the methods are setIcon() and icon(). You can set the button's icon at run time with the first method. The second method allows you to retrieve the icon of a given button. There's also a third method related to icons. It's called .setIconSize() and allows you to manipulate the icon size.

Here's an example that illustrates how to use these methods:

python
import sys

from PyQt6.QtCore import QSize
from PyQt6.QtGui import QIcon
from PyQt6.QtWidgets import QApplication, QPushButton, QVBoxLayout, QWidget

class Window(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.btnOne = QPushButton(
            icon=QIcon("./icons/logo.svg"), text="Click me!", parent=self
        )
        self.btnOne.setFixedSize(100, 60)
        self.btnOne.clicked.connect(self.onClick)
        self.btnTwo = QPushButton(parent=self)
        self.btnTwo.setFixedSize(100, 60)
        self.btnTwo.setEnabled(False)
        self.btnTwo.clicked.connect(self.onClick)
        layout = QVBoxLayout()
        layout.addWidget(self.btnOne)
        layout.addWidget(self.btnTwo)
        self.setLayout(layout)

    def onClick(self):
        sender = self.sender()
        icon = sender.icon()

        if sender is self.btnOne:
            sender.setText("")
            sender.setIcon(QIcon())
            sender.setEnabled(False)
            self.btnTwo.setEnabled(True)
            self.btnTwo.setText("Click me!")
            self.btnTwo.setIcon(icon)
            self.btnTwo.setIconSize(QSize(20, 20))
        elif sender is self.btnTwo:
            sender.setText("")
            sender.setIcon(QIcon())
            sender.setEnabled(False)
            self.btnOne.setEnabled(True)
            self.btnOne.setText("Click me!")
            self.btnOne.setIcon(icon)
            self.btnOne.setIconSize(QSize(30, 30))

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = Window()
    window.show()
    sys.exit(app.exec())

python
import sys

from PySide6.QtCore import QSize
from PySide6.QtGui import QIcon
from PySide6.QtWidgets import QApplication, QPushButton, QVBoxLayout, QWidget

class Window(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.btnOne = QPushButton(
            icon=QIcon("./icons/logo.svg"), text="Click me!", parent=self
        )
        self.btnOne.setFixedSize(100, 60)
        self.btnOne.clicked.connect(self.onClick)
        self.btnTwo = QPushButton(parent=self)
        self.btnTwo.setFixedSize(100, 60)
        self.btnTwo.setEnabled(False)
        self.btnTwo.clicked.connect(self.onClick)
        layout = QVBoxLayout()
        layout.addWidget(self.btnOne)
        layout.addWidget(self.btnTwo)
        self.setLayout(layout)

    def onClick(self):
        sender = self.sender()
        icon = sender.icon()

        if sender is self.btnOne:
            sender.setText("")
            sender.setIcon(QIcon())
            sender.setEnabled(False)
            self.btnTwo.setEnabled(True)
            self.btnTwo.setText("Click me!")
            self.btnTwo.setIcon(icon)
            self.btnTwo.setIconSize(QSize(20, 20))
        elif sender is self.btnTwo:
            sender.setText("")
            sender.setIcon(QIcon())
            sender.setEnabled(False)
            self.btnOne.setEnabled(True)
            self.btnOne.setText("Click me!")
            self.btnOne.setIcon(icon)
            self.btnOne.setIconSize(QSize(30, 30))

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = Window()
    window.show()
    sys.exit(app.exec())

In this example, we create an app with two buttons. Both buttons are connected to the onClick() method. Inside the method, we first get the clicked button using the sender() method on our app's window, self.

Next, we get a reference to the sender button's icon using the icon() method. The if statement checks if the clicked object was btnOne. If that's the case, then we reset the icon with setIcon() and disable the button with setEnabled(). The following four lines enable the btnTwo button, set its text to "Click me!", change the button's icon, and resize the icon. The elif clause does something similar, but this time the target button is btnOne.

If you run this application, then you'll get a window like this:

Window with two buttons, the top with a icon & label, the bottom empty. Click to toggle which button has the label & icon._ Window with two buttons, the top with a icon & label, the bottom empty. Click to toggle which button has the label & icon.

When we click the top button, the bottom button's text and icon will be set to Click me! and to the PythonGUIs.com logo, respectively. At the same time, the top button's text and icon will disappear. Note that the logo's size will change as well.

Another useful feature of buttons is that you can assign them a keyboard shortcut for those users that prefer the keyboard over the mouse. These methods are .setShortcut() and .shortcut(). Again, you can use the first method to set a shortcut and the second method to get the shortcut assigned to the underlying button.

These methods are commonly helpful in situations where we have a button that doesn't have any text. Therefore we can't assign it an automatic shortcut using the ampersand character &.

Checking the Status of a Button

Sometimes you'd need to check the status of a given button and take action accordingly. The QPushButton class provides a few methods that can help you check different properties related to the current status of your buttons:

Property Description Access Method Setter Method
down Indicates whether the button is pressed down or not isDown() setDown()
checked Indicates whether the button is checked or not isChecked() setChecked()
enabled Indicates whether the button is enabled or not isEnabled() setEnabled()

The down status is typically transitory and naturally happens between the pressed and released statuses. However, we can use the setDown() method to manipulate the down status at runtime.

The checked status is only available when we use checkable buttons. Only checkable buttons can be at either the checked or unchecked status.

Finally, when we enable or disable a given button, we allow or disallow the user's click on the button. In other words, disabled buttons don't respond to the user's clicks or other events, while enabled buttons do respond.

Here's an example that shows how these three sets of statuses work:

python
import sys

from PyQt6.QtWidgets import QApplication, QPushButton, QVBoxLayout, QWidget

class Window(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.btnOne = QPushButton(text="I'm Down!", parent=self)
        self.btnOne.setFixedSize(150, 60)
        self.btnOne.setDown(True)
        self.btnOne.clicked.connect(self.onBtnOneClicked)

        self.btnTwo = QPushButton(text="I'm Disabled!", parent=self)
        self.btnTwo.setFixedSize(150, 60)
        self.btnTwo.setEnabled(False)

        self.btnThree = QPushButton(text="I'm Checked!", parent=self)
        self.btnThree.setFixedSize(150, 60)
        self.btnThree.setCheckable(True)
        self.btnThree.setChecked(True)
        self.btnThree.clicked.connect(self.onBtnThreeClicked)

        layout = QVBoxLayout()
        layout.addWidget(self.btnOne)
        layout.addWidget(self.btnTwo)
        layout.addWidget(self.btnThree)
        self.setLayout(layout)

    def onBtnOneClicked(self):
        if not self.btnOne.isDown():
            self.btnOne.setText("I'm Up!")
            self.btnOne.setDown(False)

    def onBtnThreeClicked(self):
        if self.btnThree.isChecked():
            self.btnThree.setText("I'm Checked!")
        else:
            self.btnThree.setText("I'm Unchecked!")

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = Window()
    window.show()
    sys.exit(app.exec())

python
import sys

from PySide6.QtWidgets import QApplication, QPushButton, QVBoxLayout, QWidget

class Window(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.btnOne = QPushButton(text="I'm Down!", parent=self)
        self.btnOne.setFixedSize(150, 60)
        self.btnOne.setDown(True)
        self.btnOne.clicked.connect(self.onBtnOneClicked)

        self.btnTwo = QPushButton(text="I'm Disabled!", parent=self)
        self.btnTwo.setFixedSize(150, 60)
        self.btnTwo.setEnabled(False)

        self.btnThree = QPushButton(text="I'm Checked!", parent=self)
        self.btnThree.setFixedSize(150, 60)
        self.btnThree.setCheckable(True)
        self.btnThree.setChecked(True)
        self.btnThree.clicked.connect(self.onBtnThreeClicked)

        layout = QVBoxLayout()
        layout.addWidget(self.btnOne)
        layout.addWidget(self.btnTwo)
        layout.addWidget(self.btnThree)
        self.setLayout(layout)

    def onBtnOneClicked(self):
        if not self.btnOne.isDown():
            self.btnOne.setText("I'm Up!")
            self.btnOne.setDown(False)

    def onBtnThreeClicked(self):
        if self.btnThree.isChecked():
            self.btnThree.setText("I'm Checked!")
        else:
            self.btnThree.setText("I'm Unchecked!")

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = Window()
    window.show()
    sys.exit(app.exec())

In this example, we first set btnOne down using the setDown() method. Then we disable btnTwo using the setEnabled() with False as an argument. This will make this button unresponsive to user events. Finally, we set btnThree as checkable with setCheckable(). Being checkable means that we can use the checked and unchecked statuses in our code.

The onBtnOneClicked() method is connected to btnOne. This method checks if the button is not down and changes the button text accordingly.

The onBtnThreeClicked() is connected to btnThree. This method alternatively changes the button's text depending on its checked status.

If you run this app, you'll get the following window:

Window with 3 buttons: one starting in the down state, one disabled and one checked & toggleable. Window with 3 buttons: one starting in the down state, one disabled and one checked & toggleable.

First, note that these three buttons have different tones of gray. These different tones of gray indicate three different states. The first button is down, the second button is disabled, and the third button is checked.

If you click the first button, then it'll be released, and its text will be set to I'm Up!. The second button won't respond to your clicks or actions. The third button will alternate its status between unchecked and checked.

Exploring Other Properties of Button Objects

QPushButton has several other useful properties we can use in our GUI applications. Some of these properties with their corresponding setter and getter method include:

Property Description Access Method Setter Method
default Indicates whether the button is the default button on the containing window or not isDefault() setDefault()
flat Indicates whether the button border is raised or not isFlat() setFlat()

The default property comes in handy when you have several buttons on a window, and you need to set one of these buttons as the default window's action. For example, say we have a dialog with the Ok and Cancel buttons. In this case, the Ok button can be your default action.

The flat property is closely related to the look and feel of your app's GUI. If we set flat to True, then the button's border won't be visible.

Associating a Popup Menu to a Button

QPushButton objects can also display a menu when you click them. To set up this menu, you must have a proper popup menu beforehand. Then you can use the setMenu() method to associate the menu with the button.

Here's an example that creates a button with an attached popup menu:

python
import sys

from PyQt6.QtWidgets import (
    QApplication,
    QMenu,
    QPushButton,
    QVBoxLayout,
    QWidget,
)

class Window(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.btnOne = QPushButton(text="Menu!", parent=self)

        self.menu = QMenu(self)
        self.menu.addAction("First Item")
        self.menu.addAction("Second Item")
        self.menu.addAction("Third Item")

        self.btnOne.setMenu(self.menu)

        layout = QVBoxLayout()
        layout.addWidget(self.btnOne)
        self.setLayout(layout)

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = Window()
    window.show()
    sys.exit(app.exec())

python
import sys

from PySide6.QtWidgets import (
    QApplication,
    QMenu,
    QPushButton,
    QVBoxLayout,
    QWidget,
)

class Window(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.btnOne = QPushButton(text="Menu!", parent=self)

        self.menu = QMenu(self)
        self.menu.addAction("First Item")
        self.menu.addAction("Second Item")
        self.menu.addAction("Third Item")

        self.btnOne.setMenu(self.menu)

        layout = QVBoxLayout()
        layout.addWidget(self.btnOne)
        self.setLayout(layout)

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = Window()
    window.show()
    sys.exit(app.exec())

In this example, we create a button with an associated popup menu. To attach the menu to the target button, we use setMenu(), which turns the button into a menu button.

If you run this application, then you'll get a window that will look something like this:

Window with a single button, with attached drop-down menu. Window with a single button, with attached drop-down menu.

In some window styles, the button will show a small triangle on the right end of the button. If we click the button, then the pop-up menu will appear, allowing us to select any available menu option.

Conclusion

Push buttons are pretty useful widgets in any GUI application. Buttons can respond to the user's events and perform actions in our applications.

In this tutorial, you've learned how to create, use, and customize your button while building a GUI application.

Product-led Growth and Product Analytics, Can There Be One Without The Other?

What is Product-Led Growth for Embedded Devices?

Product-led growth puts the product experience, both from a software and a hardware perspective, into the focus of the go-to-market strategy. Instead of planning sales and marketing operations around high-touch customer engagements and marketing campaigns, the digital and physical experience of the embedded device is such that customers proactively engage in the purchase process.

Easy product onboarding, quick time to value, and a superior understanding of which features provide continuous value to end users are key to a successful product-led go-to-market execution. Can a Product Manager achieve these three success criteria without product analytics?

Grantlee version 5.3.1 now available

I’ve just made a new 5.3.1 release of Grantlee. The 5.3.0 release had some build issues with Qt 6 which should now be resolved with version 5.3.1.

Unlike previous releases, this release will not appear on http://www.grantlee.org/downloads/. I’ll be turning off grantlee.org soon. All previous releases have already been uploaded to https://github.com/steveire/grantlee/releases.

The continuation of Grantlee for Qt 6 is happening as KTextTemplate so as not to be constrained by my lack of availability. I’ll only make new Grantlee patch releases as needed to fix any issues that come up in the meantime.

Many thanks to the KDE community for taking up stewardship and ownership of this library!

Understanding qAsConst and std::as_const

Every now and then, when I submit some code for a code review, people tell me that I forgot qAsConst.

Now I have one more enemy, namely: Clazy! It has also started saying this, and I guess it’s about time for me to figure out what is going on. When do I need qAsConst and why do I need to know these things?

The Dreaded Warning

The following code is an example of where Clazy and my coworkers tell me to add qAsConst:

class MyClass {
  QStringList m_list;
public:
  void print() {
     for (const QString &str : m_list)  // WARNING: might detach, add qAsConst
        print(str);
  }
};

Why do I need qAsConst there? Let me tell you a little bit about what’s happening behind the scenes, and that will allow us to understand why qAsConst is needed.

Clazy is complaining there regarding a very peculiar interaction that we have between the C++11 range-based for loop that we have there and a Qt container, in this case QStringList. But this can apply to any Qt container.

What is the (potential) problem with that code that Clazy is warning about? Clazy is warning that the code, written like that, could perform a hidden copy of your container. If you just take your container and run through the container, there’s no copy in here. It looks like there is no copy, but there could be a copy. Why would you need a copy here?


It all boils down to the fact that Qt containers are implicitly shared or, as we say, Copy On Write (or COW 🐮). In a nutshell, that means that inside each and every Qt container there is a little number — a reference counter. Every time you try to modify a container, the first thing that Qt does is checks that number.

If the number is bigger than 1, then Qt does a deep copy of that container. We refer to this operation as a detach of the container. That code, written like that, could detach. That’s the kind of warning that Clazy is giving you. Since it looks like that, you’re not modifying the container in any way. You’re just iterating over the StringList, just printing the strings.

Clazy is getting a bit suspicious, thinking, “Okay, so you’re taking a copy of the container but you don’t want to modify the container? Why are you taking the copy in the first place?”


Where on this particular line does it even have a chance to detach? It’s not visible, but it’s behind the scenes.

It has to do with the new for loop. How is it implemented? Or even better, what is it equivalent to? Do you know what the new for loop expands to when the compiler sees that and compiles it?

For loops on containers have always been about iterators. You get an iterator pointing to the first element, an iterator beyond the last element, and then you iterate through. You’d assume some iterators and then whatever you’re pointing to is copied into your reference, or you’d get a QString reference to that element that it’s pointed to.

How does the range-based for loop iterate on an arbitrary container? You’ve guessed it right, it still uses iterators, even if they’re not visible to us. Given a container, the new for loop calls begin() and end() on it, in order to get the iterators that are necessary to actually transverse the container.

Here’s the catch: if the container is a Qt container, and it is non-const, then those operations are operations that do the little game I was playing; that is, they go inside the Qt container, check if the reference counter is bigger than 1, and, if it is, detach.

So the very act of calling begin() and end() could detach. As I said, it’s not visible but it’s there, hidden inside the range-based for loop.

How to Solve the Problem

Make the Container const

When using a Qt container, a necessary condition for a detach to happen is that we are calling methods such as begin() and end() on a container which is not const. Therefore, the simplest approach to fix the whole problem is to make that container const somehow. We could, for instance, take a copy — a const copy, of course, or even better, a const reference to the container.

class MyClass {
  QString m_list;
public:
  void print() {
     const QStringList copy = m_list;
     for (const QString &str : copy) // OK
        print(str);
  }
};

Here we’re taking a const copy of the container. Now, taking a copy is cheap, given this is a Qt container. The copy on write mechanism implies that the act of taking a copy simply increments the reference counter. (If you take a const reference, you’re not even paying for that.)

But now the key aspect is that the for loop is acting on a const container: the begin() that it’s going to call is going to be the begin() overload for const containers, which does not detach. Since you have a const container, there’s no chance for you to modify the container because it’s const. So there is no need to detach at all. And, indeed, this will suppress the warning from Clazy because you cannot detach anymore.

There are better ways, of course. There are a couple of other things that you can do.

Mark the Entire Method As const

The first thing you can do better is realize that the entire print function actually doesn’t need to modify this object. So you could mark the entire method as const and that would, as a by-product, make the member m_list const. So, you’re good to go.

class MyClass {
  QStringList m_list;
public:
  void print() const {
     for (const QString &str : m_list) 
        print(str);
  }
};

That would be the best solution. But, of course, it’s not general — maybe you also need to do something else and modify this, so you cannot mark a given method as const.

qAsConst or std::as_const

The second best solution is using something like qAsConst or, equivalently, std::as_const on top of your m_list object.

class MyClass {
  QStringList m_list;
public:
  void print() {
     for (const QString &str : std::as_const(m_list))
        print(str);
  }
};

qAsConst and std::as_const do exactly the same thing. std::as_const is C++17, so if you’re not on 17 just yet, you can use the Qt version. What they do is simply give you back a const reference to that container.

What do these functions do? They’re just like a const_cast — they’re taking a reference to m_list and adding a const on top of it. So it’s just a manipulation of the type, something that’s done just for the purpose of the compiler. Now the compiler knows that the thing it gets out of qAsConst is, of course, a reference to a const QStringList; so it can call only the const begin() and the const end(). As we have discussed, those calls will not detach your container.

This solution doesn’t cost you anything at runtime. It does not generate any additional code or anything of that sort. It is purely a type manipulation done for the compiler.

Corner Cases

There are some corner cases worth discussing.

Suppose that you have a method that generates a container and you call that method from a range-based for loop because you want to iterate over its results:

QStringList generateStrings();

void print() {
   for (const QString &str : generateStrings()) // Clazy warning here
      print(str);
}

Of course Clazy is going to give you a warning there, because you could be detaching the container returned by the function call. So you try to wrap the call in qAsConst:

QStringList generateStrings();

void print() {
  for (const QString &str : qAsConst(generateStrings())) // ERROR
    print(str);
}

This code doesn’t compile. The reason is a bit complicated, because it has to do with the C++ rules around what exactly you can feed into qAsConst. To keep it simple, you’re not allowed to place things like function calls inside qAsConst or, equivalently, inside std::as_const. It’s actually a good thing that you can’t do that because, otherwise, your program would crash at runtime.

Declare a Local QStringList

Fortunately, for this specific use case, there is a better solution. You can simply declare a local QStringList. Make it const, of course, because that’s the purpose of the whole exercise. Then, pass the local QStringList into your for loop. For instance:

QStringList generateStrings();

void print() {
  const QStringList list = generateStrings();
  for (const QString &str : list) // OK
    print(str);
}

In C++20, I would actually not even need an extra line to declare list. A nice thing about C++20, as a kind of smaller convenience, it added the possibility of adding an initialization inside the range-based for loop. This brings it slightly closer to the old for loop, when you always had the first statement, then the guard, and then the increment.

So you could write something like this, to spare you having this variable around for longer than the loop itself:

QStringList generateStrings();

void print() {
  for (const QStringList list = generateStrings(); const QString &str : list) // OK
    print(str);
}

Why does this problem exist to begin with? What if generateStrings() generates and returns a list and there can’t be anyone else who has that string list?

QStringList generateStrings() {
  QStringList result;
  result << "hello" << "world";
  return result;
}

void print() {
   for (const QString &str : generateStrings()) // Why a warning? The returned list isn't shared with anyone!
      print(str);
}

You, as a human being, can reason on the code flow and see that the QStringList returned by generateStrings() is not shared. Its reference counter is going to be 1, and that cannot detach. But that’s why this is a warning: Clazy is just telling you that this could detach. You have to remember that, in the general case, your generateStrings() function could not be returning a brand new QStringList.

Maybe it’s returning a QStringList that you have elsewhere. If that’s the case, that return from generateStrings() would implicitly increase the reference counter of your QStringList, because you’re creating a new copy. You would actually detach.

Inside your for loop, you would have a QStringList with a reference counter bigger than 1. Clazy, of course, does not have a crystal ball; it cannot predict what your code is going to do. So it’s just warning you by telling you this code might detach — that there is the possibility. It is actually a fairly concrete possibility, given the fact that, typically, when dealing with Qt containers, we always return them by value.

Even if doesn’t actually detach right now, one day we might refactor the code. We may say something like, “my string list has to survive the generateStrings() method. Maybe I want to cache it. Maybe I want to save that work”. That day, the reference counter is going to increase and the warning would actually be a good warning.

Return a const QStringList?

Instead of just returning a QStringList, generateStrings() could return a const QStringList:

const QStringList generateStrings();

void print() {
   for (const QString &str : generateStrings()) // OK
      print(str);
}

Clazy will not complain here, again because we cannot detach a const container.

However: this is a bad idea. You shouldn’t be returning const things from functions, and especially const containers. Why is that? Because you’re going to break move semantics.

C++11 introduced move semantics, which allow you to optimize, in general, the return from functions. When you call a function and get a value back, you’re going to get a temporary built as result of the function call. You can then move from that temporary into, let’s say, a local object. Moving containers is usually very cheap! That’s definitely something we want to keep.

If you return const objects however that move gets broken; you can’t move from a const object. What happens then? You’re going to perform a copy instead. Now, in the case of Qt containers, you can get away with that because copying a Qt container is not particularly expensive. All that you really do is increase the reference counter.

So, as a practical workaround, that could work. But consider this: the moment you decide you don’t like QStringList any more and decide to make it a std::vector or something more complicated (some non-Qt container of any sort), you’re going to pay for that const because copying a standard container and not moving it is going to cost you a lot. So it’s very important to be careful about what you do.

Return a std::vector

You wouldn’t have this problem at all with std::vector. The reason for that is that std::vector, and actually all Standard Library containers, are not reference counted; they do not implement this mechanism. Among these other things, this means that a call to begin() into a std::vector object will never copy it behind the scenes. You’ll be absolutely fine, if your generateStrings() function returns a std::vector.

std::vector<QString> generateStrings();

void print() {
   for (const QString &str : generateStrings()) // OK
      print(str);
}

Summary

To summarize, you need qAsConst() when you iterate in a range-based for loop. That’s the first thing. And you need qAsConst when what you’re iterating over is non-const.

It could be const if you’re in a const method (and the variable is a data member of this) or if it’s a variable that you declared as const elsewhere. Otherwise, you need qAsConst but you can only use qAsConst if it’s an l-value, something that has a local name, and not the return of a function.

If you are iterating over an object that you get from a function call, save it in a local variable instead (and make it const).

Mutating Iteration

The Clazy warning stems from the fact that your for loop doesn’t want to modify your container. In all of our examples, we were using just a debug, or it seemed like we were just reading from the container. Clearly, if you do want to modify the container, then you don’t want to apply const at any level! You would need to be working on a non-const container.

But the key aspect of mutating iteration is that, in this case, your iteration variable would have a different type. As you can see, right now we’re taking a const QString reference. That means that we cannot mutate the container contents through that variable. If you need to modify the container itself, you’d be taking a non-const QString reference! In that case, you would not get the warning from Clazy, because it would detect that you want to modify the container. If that means a detach, that’s the price to pay — you have a container that’s just a shallow copy to some shared data and, if you want to modify it, you need to create your own copy of that data.

In C++23 there is yet another solution that will work in even more cases. We’ll cover that at a later time, either on our YouTube channel or as a blog, or both. We’ll keep you posted, so please stay tuned.

About KDAB

If you like this article and want to read similar material, consider subscribing via our RSS feed.

Subscribe to KDAB TV for similar informative short video content.

KDAB provides market leading software consulting and development services and training in Qt, C++ and 3D/OpenGL. Contact us.

The post Understanding qAsConst and std::as_const appeared first on KDAB.

Don’t miss this event – Scythe Studio at Qt World Summit 2022

Qt World Summit is the yearly event organized by The Qt Company for all the people interested in Qt QML coding, UX/UI designing using Qt tools, quality assurance software and in general project development using Qt framework. So it doesn’t matter if you are a developer, tester, designer or manager. Qt World Summit 2022 is an event for you.
The event takes place on Wednesday 9th November from 09:00 AM to 02:00 PM CET. It’s fully online and you can register here.

KDAB and Qt World Summit 2022

KDAB will be Gold sponsors at this year’s free online edition of Qt World Summit on November 9th, 2022. Our very own Jesper Pedersen will present his talk “Highlights from Qt Widgets and More”. Don’t miss out! Join developers, designers, managers, and executives to get inspired by the latest developments with Qt.

Register now and make sure to not miss Jesper’s talk here! It will be online on November 9th, 10:45 am – 11.30 am CET

‘Highlights from Qt Widgets and More’

For more than a year Jesper Pedersen has hosted a Youtube series focusing on Qt Widgets and everything around developing with it. In this presentation, Jesper will highlight some of the most important take away’s, including coding tips, and especially those relating to the model/view framework, Qt Creator power tips, and general tips relating to software development tools.

The beauty of this presentation is that there will be material on our Youtube channel for each point where you can dive into more details if needed!

Watch all episodes on Qt Widgets and More

qt widgets and more KDAB

Click the image or visit this link

The post KDAB and Qt World Summit 2022 appeared first on KDAB.

KDDockWidgets 1.6.0 released, Is the i.MX 8 right for your project? KDAB tools and video releases

 

&

96

 

­
­
­
­
­
­
­

Welcome to September’s news from KDAB

This month we had a company meeting in Vilnius and about 100 KDABians enjoyed being together in the same space for the first time in more than a few years. There was information sharing on multiple levels, getting to know new folk, and a good deal of fun. What else happened in Vilnius stays in Vilnius but suffice it to say there were a lot of potatoes involved 😉

We bring you a potato-free newsletter, nonetheless, with Choosing a CPU: is the i.MX 8 right for your project?, the release of KDDockWidgets 1.6.0 and news of other KDAB tool releases.

We also offer new releases in the Qt Widgets and More series and a multitude of videos from, and news about Events.

­
­
­
­
­
­
­
­
­

Choosing a CPU

Is the i.MX 8 right for your project?

­
­
­
­
­

by Till Adam

­

We’ve learned some hard-won lessons in using i.MX 8 silicon to bring customer projects to life and have helped customers determine which CPU is the most appropriate for their current product and future roadmap.

In this blog, we share some of our CPU choice considerations, helping eliminate some of the unknowns and hopefully clearing away some misconceptions in the process.

Read the blog.

­
­
­
­
­
­
­
­
­
­

KDDockWidgets 1.6.0 released

now supporting Qt 6.2

­
­
­
­
­

Our framework for custom-tailored docking systems in Qt got a major upgrade this month. Whilst it still, of course, supports Qt 5, now Qt 6 is supported from version 6.2.0. And there’s more.

Check out the release notes on github.

Find out more about KDDockWidgets.

­
­
­
­
­
­
­
­
­
­
­
­
­
­
­
­

KDAB tools re-licensed to MIT

KDAB recently made the decision to relicense many of our free tools from GPL to the less restrictive MIT. These are the tools we released this month – they can all be downloaded free.

­
­
­
­
­
­

KDChart 3.0.0

KDChart is a comprehensive business charting package with many different chart types and a large number of customization options.

KDChart 3 is the first release of KDChart that supports Qt 6, and both Qt 5 and Qt 6 can be co-installed.

Check out the release highlights.

Find out more and download KDChart.

­
­
­
­
­
­
­
­
­
­

KDSoap 2.1.0

KDSoap 2.1.0 was also released this September.

KD SOAP is a tool for creating client applications for web services. Among other things, KD SOAP makes it possible to interact with applications which have APIs that can be exported as SOAP objects. The web service then provides a machine-accessible interface to its functionality via HTTP.

Check out the release highlights.

Find out more and download KDSoap.

­
­
­
­
­
­
­
­
­
­

KDReports 2.2.0

KD Reports creates all kinds of reports from within Qt applications, generating printable and exportable reports from code and from XML descriptions.

Reports may contain text paragraphs, tables, headlines, charts, headers and footers and more.

KDReports 2.2.0 release notes are here.

Find out more and download KDReports.

­
­
­
­
­
­
­
­
­
­
­
­
­

Qt Widgets & More Releases

­
­
­
­
­

We kick off with two episodes where Jesper and Giuseppe D’Angelo have fun sharing their knowledge with you: Which String Class in Qt should I use? and QStringBuilder.

Then we have Save Re-compile Time – Include moc Files in Source Files, where Jesper explains how he went from 22 seconds to 5 when recompiling.

Finally, in Improving my Clang-Tidy Checks, Jesper reveals some of the not-so-welcome surprises he got from checking checks – and what he did about it.

Access the Qt Widgets and More playlist.

­
­
­
­
­

Which String Class in Qt should I use?

­
­

Save Re-compile Time – Include moc Files in Source Files

­
­
­
­

QStringBuilder

­
­

Improving my Clang-Tidy Checks

­
­
­
­
­
­
­
­
­
­
­
­

Video Releases from Qt Developer Conference

­

We released twelve more videos since the last Newsletter. They are all excellent. Take a look at the four randomly selected below, but see the full list here. There’s more to come!

­
­
­
­
­
­

KDE’s journey to Qt 6, by Nicolas Fella.

Since 25 years, code developed by the KDE Community has been ported to Qt. The next major version transition is coming with Qt 6.

Hybrid Qt Development: Boosting Your Projects with Python, by Dr Christián Maureira-Fredes.

About adding new technology to the Qt ecosystem.

25k Stars Later – How to Survive Maintaining a Popular GitHub Project in Your Spare Time, by C++ developer Niels Lohmann. The title says it well.

How Can I Make My Qt Apps More Rusty?

Andrew Hayzen and Leon Mathes offer a practical approach for adding Rust to existing C++ projects.

­
­
­
­
­

KDE’s Journey to Qt 6

­
­

25k Stars Later – How to Survive Maintaining a Popular GitHub Project in Your Spare Time

­
­
­
­

Boosting Your Projects with Python

­
­

How Can I Make My Qt Apps More Rusty?

­
­
­
­
­
­
­
­
­
­
­

ACCU Video Releases

­

Here are a couple of talks given by KDAB’s James Turner at ACCU earlier this year. We somehow missed them when they came out, so we thought maybe you did as wel

How Code Fails in the Real World

– about bugfixing an open source project: the horrors, the solutions.

Properties and Bindings for Modern C++

– about getting Qt’s declarative language goodness into C++.

Sit down and enjoy a highly informative and fun ride.

There’s a playlist for ACCU here.

­
­
­
­
­
­
­
­
­
­
­
­
­
­
­
­
­
­
­

More on Events

CppCon happened last week (see the pre-release videos here), KDE Akademy is about to happen (Oct 1 – 7), and we are proud to have sponsored both events.

­
­
­
­

This month, instead of the usual rundown of what’s coming up, we decided to simply share our KDAB News report where you’ll find out about Qt World Summit (November 9th, plus meetups later) as well as other news.

Most importantly though, founder Jens Weller talks about Meeting C++ (November 17 – 19), and what it’s like running such an event 10 years on.

While we’re on the subject, if C++ is your bag, you might also like to watch or listen to this – an in-depth discussion on the Future of C++ – an international panel discussion with C++ experts.

­
­
­
­
­
­
­
­ ­
­
­

 

The post KDDockWidgets 1.6.0 released, Is the i.MX 8 right for your project? KDAB tools and video releases appeared first on KDAB.

Getting started with VS Code for Python

Setting up a working development environment is the first step for any project. Your development environment setup will determine how easy it is to develop and maintain your projects over time. That makes it important to choose the right tools for your project. This article will guide you through how to set up Visual Studio Code, which is a popular free-to-use, cross-platform code editor developed by Microsoft, in order to develop Python applications.

Visual Studio Code is not to be confused with Visual Studio, which is a separate product also offered by Microsoft. Visual Studio is a fully-fledged IDE that is mainly geared towards Windows application development using C# and the .NET Framework.

Setup a Python environment

In case you haven't already done this, Python needs to be installed on the development machine. You can do this by going to python.org and grabbing the specific installer for either Windows or macOS. Python is also available for installation via Microsoft Store on Windows devices.

Make sure that you select the option to Add Python to PATH during installation (via the installer).

If you are on Linux, you can check if Python is already installed on your machine by typing python3 --version in a terminal. If it returns an error, you need to install it from your distribution's repository. On Ubuntu/Debian, this can be done by typing sudo apt install python3. Both pip (or pip3) and venv are distributed as separate packages on Ubuntu/Debian and can also be installed by typing sudo apt install python3-pip python3-venv.

Setup Visual Studio Code

First, head over to to code.visualstudio.com and grab the installer for your specific platform.

If you are on a Raspberry Pi (with Raspberry Pi OS), you can also install VS Code by simply typing sudo apt install code. On Linux distributions that support Snaps, you can do it by typing sudo snap install code --classic.

Once VS Code is installed, head over to the Extensions tab in the sidebar on the left by clicking on it or by pressing CTRL+SHIFT+X. Search for the 'Python' extension published by Microsoft and click on Install.

The Extensions tab in the left-hand sidebar The Extensions tab in the left-hand sidebar.

Usage and Configuration

Now that you have finished setting up VS Code, you can go ahead and create a new Python file. Remember that the Python extension only works if you open a .py file or have selected the language mode for the active file as Python.

To change the language mode for the active file, simply press CTRL+K once and then press M after releasing the previous keys. This kind of keyboard shortcut is called a chord in VS Code. You can see more of them by pressing CTRL+K CTRL+S (another chord).

The Python extension in VS Code allows you to directly run a Python file by clicking on the 'Play' button on the top-right corner of the editor (without having to type python file.py in the terminal).

You can also do it by pressing CTRL+SHIFT+P to open the Command Palette and running the > Python: Run File in Terminal command.

Finally, you can configure VS Code's settings by going to File > Preferences > Settings or by pressing CTRL+COMMA. In VS Code, each individual setting has an unique identifier which you can see by clicking on the cog wheel that appears to the left of each setting and clicking on 'Copy Setting ID'. This ID is what will be referred to while talking about a specific setting. You can also search for this ID in the search bar under Settings.

Linting and Formatting Support (Optional)

Linters make it easier to find errors and check the quality of your code. On the other hand, code formatters help keep the source code of your application compliant with PEP (Python Enhancement Proposal) standards, which make it easier for other developers to read your code and collaborate with you.

For VS Code to provide linting support for your projects, you must first install a preferred linter like flake8 or pylint.

bash
pip install flake8

Then, go to Settings in VS Code and toggle the relevant setting (e.g. python.linting.flake8Enabled) for the Python extension depending on what you installed. You also need to make sure that python.linting.enabled is toggled on.

A similar process must be followed for code formatting. First, install something like autopep8 or black.

bash
pip install autopep8

You then need to tell VS Code which formatter to use by modifying python.formatting.provider and toggle on editor.formatOnSave so that it works without manual intervention.

If pip warns that the installed modules aren't in your PATH, you may have to specify the path to their location in VS Code (under Settings). Follow the method described under Working With Virtual Environments to do that.

Now, when you create a new Python file, VS Code automatically gives you a list of Problems (CTRL+SHIFT+M) in your program and formats the code on saving the file.

Identified problems in the source code. Identified problems in the source code, along with a description and line/column numbers.

You can also find the location of identified problems from the source overview on the right hand, inside the scrollbar.

Working With Virtual Environments

Virtual environments are a way of life for Python developers. Most Python projects require the installation of external packages and modules (via pip). Virtual environments allow you to separate one project's packages from your other projects, which may require a different version of those same packages. Hence, it allows all those projects to have the specific dependencies they require to work.

The Python extension makes it easier for you by automatically activating the desired virtual environment for the in-built terminal and Run Python File command after you set the path to the Python interpreter. By default, the path is set to use the system's Python installation (without a virtual environment).

To use a virtual environment for your project/workspace, you need to first make a new one by opening a terminal (View > Terminal) and typing python -m venv .venv. Then, you can set the default interpreter for that project by opening the Command Palette (CTRL+SHIFT+P) and selecting > Python: Select Interpreter.

You should now either close the terminal pane in VS Code and open a new one or type source .venv/bin/activate into the existing one to start using the virtual environment. Then, install the required packages for your project by typing pip install <package_name>.

VS Code, by default, looks for tools like linters and code formatters in the current Python environment. If you don't want to keep installing them over and over again for each new virtual environment you make (unless your project requires a specific version of that tool), you can specify the path to their location under Settings in VS Code. - flake8 - python.linting.flake8Path - autopep8 - python.formatting.autopep8Path

To find the global location of these packages on macOS and Linux, type which flake8 and which autopep8 in a terminal. If you are on Windows, you can use where <command_name>. Both these commands assume that flake8 and autopep8 are in your PATH.

Understanding Workspaces in VS Code

VS Code has a concept of Workspaces. Each 'project folder' (or the root/top folder) is treated as a separate workspace. This allows you to have project-specific settings and enable/disable certain extensions for that workspace. It is also what allows VS Code to quickly recover the UI state (e.g. files that were previously kept open) when you open that workspace again.

In VS Code, each workspace (or folder) has to be 'trusted' before certain features like linters, autocomplete suggestions and the in-built terminal are allowed to work.

In the context of Python projects, if you tend to keep your virtual environments outside the workspace (where VS Code is unable to detect it), you can use this feature to set the default path to the Python interpreter for that workspace. To do that, first Open a Folder (CTRL+K CTRL+O) and then go to File > Preferences > Settings > Workspace to modify python.defaultInterpreterPath.

Setting the default interpreter path for the workspace. Setting the default interpreter path for the workspace.

In VS Code settings you can search for settings by name using the bar at the top.

You can also use this approach to do things like use a different linter for that workspace or disable the code formatter for it. The workspace-specific settings you change are saved in a .vscode folder inside that workspace, which you can share with others.

If your VS Code is not recognizing libraries you are using in your code, double check the correct interpreter is being used. You can find which Python version you're using on the command line by running which python or which python3 on macOS/Linux, or where python or where python3 on Windows.

Working With Git in VS Code (Optional)

Using Version Control is required for developing applications. VS Code does have in-built support for Git but it is pretty barebones, not allowing much more than tracking changes that you have currently made and committing/pushing those changes once you are done.

For the best experience, it is recommended to use the GitLens extension. It lets you view your commit history, check who made the changes and much more. To set it up, you first need to have Git set up on your machine (go here) and then install GitLens from the Extensions tab in the sidebar on the left. You can now use those Git-related features by going to the Git tab in the sidebar (CTRL+SHIFT+G).

There are more Git-related extensions you could try as well, like Git History and GitLab Workflow. Give them a whirl too!

Community-driven & open source alternatives

While VS Code is open source (MIT-licensed), the distributed versions include some Microsoft-specific proprietary modifications, such as telemetry (app tracking). If you would like to avoid this, there is also a community-driven distribution of Visual Studio Code called VSCodium that provides freely-licensed binaries without telemetry.

Due to legal restrictions, VSCodium is unable to use the official Visual Studio Marketplace for extensions. Instead, it uses a separate vendor neutral, open source marketplace called Open VSX Registry. It doesn't have every extension, especially proprietary ones, and some are not kept up-to-date but both the Python and GitLens extensions are available on it.

You can also use the open source Jedi language server for the Python extension, rather than the bundled Pylance language server/extension, by configuring the python.languageServer setting. You can then completely disable Pylance by going to the Extensions tab. Note that, if you are on VSCodium, Jedi is used by default (as Pylance is not available on Open VSX Registry) when you install the Python extension.

Conclusion

Having the right tools and making sure they're set up correctly will greatly simplify your development process. While Visual Studio starts as a simple tool, it is flexible and extendable with plugins to suit your own preferred workflow. In this tutorial we've covered the basics of setting up your environment, and you should now be ready to start developing your own applications with Python!

Choosing a CPU

Choosing a CPU

CPU

Is the i.MX 8 Right for Your Project?

When building an embedded systems product, among your earliest decisions is the choice of hardware. More specifically, on what CPU should you base your design? Today’s system-on-chip processors have a huge array of resources to contribute to your solution: multiple cores and on-board DSPs, graphics engines and display controllers, peripheral support and connectivity interfaces, and more. Because a new hardware platform entails a costly investment in hardware engineering, electrical design, and software development – as well as being the basis for future product spin-offs – it makes sense to consider your hardware selection wisely.

If you’re like many of our customers, you may be wondering if an i.MX 8 chip could form the core CPU of your product family since NXP has, at long last, brought the power of 64-bit processing to embedded with the i.MX 8 series. With an i.MX 8, your design would have enough horsepower to last for many product generations. Yet the i.MX 6x family, a predecessor of the i.MX 8, has been a venerable and trusted CPU for many projects over many years. With so much of your company’s future riding on your hardware selection, deciding between the two is not easy to do.

We’ve learned some hard-won lessons in using i.MX 8 silicon to bring customer projects to life and have helped customers determine which CPU is the most appropriate for their current product and future roadmap. We’ll share with you some of our CPU choice considerations, helping eliminate some of the unknowns and hopefully clearing away some misconceptions in the process.

1. Processing Power

One of the biggest updates between the i.MX 6 and i.MX 8 families is in processing power. The i.MX 6 family uses a 32-bit architecture with a clock-speed between 528 MHz (for the ULL) and 1.2 GHz (for the i.MX 6Dual/Quad). Although there are dedicated dual and quad core versions, most i.MX6 chips have a single core.

In contrast, every member of the i.MX 8 family has a 64-bit architecture running between 1.2 GHz and 1.6 GHz with up to four Cortex-A cores. In addition, all of the chips have an extra Cortex-M core; several models have an extra DSP; and the i.MX 8QuadMax has two additional Cortex-A72 cores. Clearly, there is a lot more oomph in the i.MX 8 product line!

The main question is whether you need that extra processing power. If you’ve got an existing product, look at your CPU idle time with your operating system’s performance measuring tools. Chances are good that you’re not anywhere near maxing out the existing CPU under most normal situations. Throwing extra cores at a problem only works if software tasks are well-divided in processing load and independent execution. If all of your tasks on one core are waiting for another core’s operations to complete, you may need some  software redesign more than extra CPU horsepower.

If you don’t have an existing product for comparison, it’s much harder to tell what your anticipated CPU needs will be. Cases where the i.MX 8 power may be warranted are when consolidating multiple CPUs, running hypervisors for multiple virtualized operating systems, or in high-bandwidth or big-data applications like computer vision or machine learning. For most normal “run-of-the-mill” embedded systems, something in the i.MX 6 family will probably suffice.

2. Graphical Needs

Few embedded products require a 4K resolution but, for those that do, this is a clear-cut reason to use an i.MX 8 — the i.MX 6 doesn’t have 4K support. However, 4K has a lot of pixels to manage! Our testing of the performance on the i.MX 8’s GPU shows that frame rates for most 3D scenes fall significantly short of what’s needed for smooth animation. If your product only requires 2D windows or statically rendered 3D screens, this may not be an issue; however, really testing with your desired output is the only way to know for sure. The higher the resolution and the more objects needed to compose your screen, the more challenges you’ll have in achieving decent frame rates, regardless of chip selection. You may need to, as we’ve done for our clients, perform a lot of optimization to your 3D models, shaders, and rendering pipeline to accommodate higher resolution displays.

Another potential reason to select an i.MX 8 is if you’re decoding or encoding video. There are some video codecs that are only supported on the newer hardware, for example, VP8 or VP9 decode. Check the data sheets for a full run-down of supported standards.

3. Product Maturity

With today’s ultra-complex hardware designs, you can’t expect any manufacturer to have anticipated every possible corner case until customers push the product to its limits and use it in unanticipated configurations. You should expect some revisions before everything is completely stable when you’re developing with CPUs at the cutting edge.

Building in extra time to understand and accommodate new hardware is a good practice that’s independent of any assessment of the i.MX 8 itself. No matter how dependable a company’s track record in delivering well-tested hardware, more errata sheets are issued at the beginning of a product’s lifecycle than at the end. Of course, the impact of hardware quirks or driver bugs in your project may be negligible, and that’s more likely if your design sticks to commonly used features. If you find a problem, you’ll need to analyze and understand it and then, hopefully, install new drivers, get firmware updates, and deploy hardware or software workarounds.

Given the relatively recent release of the i.MX 8 silicon, it’s prudent to consider building in some time for troubleshooting. With development cycles on the long side (two or more years to completion), there’s enough time to smoke out as-yet-undiscovered errata that could affect your project. Developing at a rapid six-month pace doesn’t give a lot of additional time to debug and develop workarounds if any hardware problems are uncovered.

4. Batteries and Heat

Faster clock speeds moving much more data also means your CPU will be generating a lot of heat. Unless your product design requires a CPU that can fry eggs, you should expect to employ thermal regulation. The i.MX 8 runs hot when you run it flat out. A passive heat dissipation design with fins and copper heat pipes will still need software assistance. This is achieved by powering down unused parts of the chip, throttling back the clock, and jumping execution between cores to keep any one core from overheating – all techniques that the chip will do automatically when configured properly. However, because all of those things slow down your throughput, it’s important when performance testing your software to remember that you can’t rely on full-speed execution. Spare processing capacity needs to be reserved to ensure the CPU doesn’t fry itself or anything near it. If you’re lucky enough to be building a plugged-in product without noise-volume restrictions, you can always employ fans for more efficient active cooling.

Excess heat is only part of the problem with a revved-up chip; it also consumes more electricity. Again, if you can rely on constant electrical power from the grid, this is a secondary concern. However, for battery-driven products this can be a serious worry. For a battery-dependent design, you’re probably going to want to go with an i.MX 6ULL, which sips current compared to its big brother, the i.MX 8. The i.MX 8M Mini is the most power-sensitive chip in the i.MX 8 lineup, and it’s still running at twice the bus width, three times the clock speed, and with three more cores than the diminutive i.MX 6ULL.

5. Support Chips

One of the really intriguing aspects of the i.MX 8 family is that, alongside the Cortex-A35/A53/A72 that runs the OS and standard tasks, is one (or more) Cortex-M8 cores. The Cortex-M is Arm’s microcontroller profile. A Cortex-M architecture has no memory management unit (so, no memory-managed OS), no cache controller (no high-speed operations), and no floating-point unit (slower number crunching).

However, it runs on very low power and is real-time responsive, making it ideal for dedicated real-time activities like reading or reacting to sensors, controlling hardware peripherals, maintaining a system watchdog, or managing battery and low-power states.

Many systems use a separate independent microcontroller to manage these tasks. With an i.MX 8, you can build it into a single chip, potentially saving cost and shrinking board space. That, in itself, is a great reason to consider a move to an i.MX 8 design.

6. Safety-Critical

Another intriguing add-on to the i.MX 8 lineup is their support for safety-critical applications. With automotive projects increasingly needing both safety critical and non-critical parts, the i.MX 8X seems tailor-made to win automotive hearts and minds, as well as any other applications that need a mix of horsepower with functional safety. Not only does the i.MX 8X provide support for error-corrected memory, but the Cortex-M core can independently access the graphical stack. That lets the Cortex-M run safety-critical processes that can run outside of the main application space yet still provide guaranteed display of critical diagnostics, warning tell tales, or any other mission-critical information. Certification of code running on the Cortex-M should also be easier with a simpler task model and deterministic execution behavior.

Variants

If the i.MX 8 series still seems appropriate for your project, you’re probably wondering which variant makes the most sense. We can’t cover every consideration in this paper, but here are the main questions for deciding on the i.MX 8 family:

Q: Do you need functional safety? Yes – i.MX 8X family: i.MX 8QuadXPlus, i.MX 8DualXPlus, i.MX 8DualX

Q: Do you need 4K video or maximum performance? Yes – i.MX 8 family: i.MX 8QuadMax, i.MX 8QuadPlus, i.MX 8DualMax

Q: Do you need decent performance but a “standard sized” user interface? Yes – i.MX 8M Nano family: i.MX 8M Nano Quad, i.MX 8M Nano QuadLite, i.MX 8M Nano Dual, i.MX 8M Nano DualLite, i.MX 8M Nano Solo, i.MX 8M Nano SoloLite (Note that contrary to the expectation you might have from the Nano naming convention, this series has more power than the Mini series.)

Q: Do you need performance with low power consumption or a budget price? Yes – i.MX 8M Mini family: i.MX 8M Mini Quad, i.MX 8M Mini QuadLite, i.MX 8M Mini Dual, i.MX 8M Mini DualLite, i.MX 8M Mini Solo, i.MX 8M Mini SoloLite

Summary

Whether you are considering an NXP i.MX 8, NXP i.MX 6, or another vendor’s silicon, there are many factors to take into account in order to make the right hardware choice. If you’d like some of our advice in narrowing down your silicon decisions, we’d be happy to help.

 

About KDAB

If you like this article and want to read similar material, consider subscribing via our RSS feed.

Subscribe to KDAB TV for similar informative short video content.

KDAB provides market leading software consulting and development services and training in Qt, C++ and 3D/OpenGL. Contact us.

The post Choosing a CPU appeared first on KDAB.

KDChart 3.0.0 Released

We just released KDChart version 3.0.0!

KDChart is a comprehensive business charting package with many different chart types and a large number of customization options. We are constantly improving the package, and have been doing so for years.

This is the first release of KDChart that supports Qt 6, and both Qt 5 and Qt 6 can be co-installed.

And get this: KDChart 3.0.0 is completely free! We’ve relicensed KDChart from the GPL to the MIT license and removed our commercial offering. This means that you can use it as you want without license restrictions. Find out more about the MIT license, here.

We’ll also be relicensing KDReports, KDSoap, and a few other KDAB products in the not-too-distant future. Feel free to subscribe to our “KDAB Blogs” RSS feed or check out the KDAB GitHub to keep track of the when and where.

For more information…

Find out more about KD Chart here.

To access KDChart 3.0.0 and see the release highlights, visit the GitHub.

Prebuilt packages for some popular Linux distributions are here: https://build.opensuse.org/project/repositories/isv:KDAB

KDAB provides homebrew formulas for Mac users, at https://github.com/KDAB/homebrew-tap.

About KDAB

If you like this article and want to read similar material, consider subscribing via our RSS feed.

Subscribe to KDAB TV for similar informative short video content.

KDAB provides market leading software consulting and development services and training in Qt, C++ and 3D/OpenGL. Contact us.

The post KDChart 3.0.0 Released appeared first on KDAB.

Flutter vs React Native vs Qt in 2022

Choosing the best framework to develop an application is a tough task. Nonetheless, that is a critical decision to make before starting the development of your next project. Currently, quite a popular method of software development is cross platform software development. What is cross platform development? This is an approach that allows you to reuse code between different operating systems like iOS and Android or macOS and Windows. Following this way of apps development is first of all cost and time effective. In multiple cases, it is a better approach than native app development. Therefore, if you want to follow this approach, reduce costs and develop apps more quickly, you need to use a cross platform framework such as Flutter, React Native or Qt. But which one to choose? In this article, we compare Flutter vs React Native vs Qt to help you answer that question.

Packaging PyQt5 applications for Linux with PyInstaller & fpm

In the previous tutorials, we've looked at packaging your PyQt5 applications for Windows and macOS -- turning them into EXE Installers and macOS bundles respectively. But to make your application truly cross-platform you should also provide installers for Linux. In this tutorial we'll look at how to do just that, first using PyInstaller to bundle our application into a executable app and then using a tool called fpm to convert that into a Linux package.

This tutorial is broken down into a series of steps, using PyInstaller to build first simple, and then more complex PyQt5 applications into Linux executables. You can choose to follow it through completely, or skip to the parts that are most relevant to your own project.

We finish off by building an Ubuntu .deb package, the usual method for distributing application on that systems. Thanks to the magic of fpm the instructions will also work for other Linux distributions, such as Redhat .rpm or Arch .pacman.

You always need to compile your app on your target system. So, if you want to create an Ubuntu package do this on Ubuntu.

Example Ubuntu Package Example Ubuntu Package

If you're impatient, you can download the Example Ubuntu Package first.

Requirements

PyInstaller works out of the box with PyQt5 and as of writing, current versions of PyInstaller are compatible with Python 3.6+. Whatever project you're working on, you should be able to package your apps. This tutorial assumes you have a working installation of Python with pip package management working.

You can install PyInstaller using pip.

bash
pip3 install PyInstaller

If you experience problems packaging your apps, your first step should always be to update your PyInstaller and hooks package the latest versions using

bash
pip3 install --upgrade PyInstaller pyinstaller-hooks-contrib

The hooks module contains package-specific packaging instructions for PyInstaller which is updated regularly.

Install in virtual environment (optional)

You can also opt to install PyQt5 and PyInstaller in a virtual environment (or your applications virtual environment) to keep your environment clean.

bash
python3 -m venv packenv

Once created, activate the virtual environment by running from the command line —

bash
call packenv\scripts\activate.bat

Finally, install the required libraries. For PyQt5 you would use —

python
pip3 install PyQt5 PyInstaller

Getting Started

It's a good idea to start packaging your application from the very beginning so you can confirm that packaging is still working as you develop it. This is particularly important if you add additional dependencies. If you only think about packaging at the end, it can be difficult to debug exactly where the problems are.

For this example we're going to start with a simple skeleton app, which doesn't do anything interesting. Once we've got the basic packaging process working, we'll extend the application to include icons and data files. We'll confirm the build as we go along.

To start with, create a new folder for your application and then add the following skeleton app in a file named app.py. You can also download the source code and associated files

python
from PyQt5 import QtWidgets

import sys

class MainWindow(QtWidgets.QMainWindow):

    def __init__(self):
        super().__init__()

        self.setWindowTitle("Hello World")
        l = QtWidgets.QLabel("My simple app.")
        l.setMargin(10)
        self.setCentralWidget(l)
        self.show()

if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    w = MainWindow()
    app.exec()

This is a basic bare-bones application which creates a custom QMainWindow and adds a simple widget QLabel to it. You can run this app as follows.

bash
python app.py

This should produce the following window (on Ubuntu).

Simple skeleton app in PyQt5 Simple skeleton app in PyQt5

Building a basic app

Now we have our simple application skeleton in place, we can run our first build test to make sure everything is working.

Open your terminal (shell) and navigate to the folder containing your project. You can now run the following command to run the PyInstaller build.

python
pyinstaller app.py

You'll see a number of messages output, giving debug information about what PyInstaller is doing. These are useful for debugging issues in your build, but can otherwise be ignored. The output that I get for running the command on my system is shown below.

bash
$ pyinstaller app.py
85 INFO: PyInstaller: 4.10
85 INFO: Python: 3.9.7
88 INFO: Platform: Linux-5.13.0-39-generic-x86_64-with-glibc2.34
89 INFO: wrote /home/martin/pyinstaller/linux2/no-datas/pyqt5/app.spec
91 INFO: UPX is not available.
91 INFO: Extending PYTHONPATH with paths
['/home/martin/pyinstaller/linux2/no-datas/pyqt5']
236 INFO: checking Analysis
240 INFO: Building because inputs changed
240 INFO: Initializing module dependency graph...
243 INFO: Caching module graph hooks...
255 INFO: Analyzing base_library.zip ...
2008 INFO: Processing pre-find module path hook distutils from '/home/martin/.local/lib/python3.9/site-packages/PyInstaller/hooks/pre_find_module_path/hook-distutils.py'.
2013 INFO: distutils: retargeting to non-venv dir '/usr/lib/python3.9'
4231 INFO: Caching module dependency graph...
4348 INFO: running Analysis Analysis-00.toc
4379 INFO: Analyzing /home/martin/pyinstaller/linux2/no-datas/pyqt5/app.py
4403 INFO: Processing module hooks...
4403 INFO: Loading module hook 'hook-PyQt5.py' from '/home/martin/.local/lib/python3.9/site-packages/PyInstaller/hooks'...
4559 WARNING: Hidden import "sip" not found!
4559 INFO: Loading module hook 'hook-xml.etree.cElementTree.py' from '/home/martin/.local/lib/python3.9/site-packages/PyInstaller/hooks'...
4560 INFO: Loading module hook 'hook-heapq.py' from '/home/martin/.local/lib/python3.9/site-packages/PyInstaller/hooks'...
4562 INFO: Loading module hook 'hook-distutils.py' from '/home/martin/.local/lib/python3.9/site-packages/PyInstaller/hooks'...
4568 INFO: Loading module hook 'hook-xml.py' from '/home/martin/.local/lib/python3.9/site-packages/PyInstaller/hooks'...
4627 INFO: Loading module hook 'hook-PyQt5.QtWidgets.py' from '/home/martin/.local/lib/python3.9/site-packages/PyInstaller/hooks'...
4709 INFO: Loading module hook 'hook-difflib.py' from '/home/martin/.local/lib/python3.9/site-packages/PyInstaller/hooks'...
4711 INFO: Loading module hook 'hook-multiprocessing.util.py' from '/home/martin/.local/lib/python3.9/site-packages/PyInstaller/hooks'...
4712 INFO: Loading module hook 'hook-sysconfig.py' from '/home/martin/.local/lib/python3.9/site-packages/PyInstaller/hooks'...
4713 INFO: Loading module hook 'hook-encodings.py' from '/home/martin/.local/lib/python3.9/site-packages/PyInstaller/hooks'...
4759 INFO: Loading module hook 'hook-PyQt5.QtGui.py' from '/home/martin/.local/lib/python3.9/site-packages/PyInstaller/hooks'...
4807 INFO: Loading module hook 'hook-lib2to3.py' from '/home/martin/.local/lib/python3.9/site-packages/PyInstaller/hooks'...
4820 INFO: Loading module hook 'hook-pickle.py' from '/home/martin/.local/lib/python3.9/site-packages/PyInstaller/hooks'...
4827 INFO: Loading module hook 'hook-PyQt5.QtCore.py' from '/home/martin/.local/lib/python3.9/site-packages/PyInstaller/hooks'...
4853 INFO: Loading module hook 'hook-distutils.util.py' from '/home/martin/.local/lib/python3.9/site-packages/PyInstaller/hooks'...
4862 INFO: Looking for ctypes DLLs
4897 INFO: Analyzing run-time hooks ...
4900 INFO: Including run-time hook '/home/martin/.local/lib/python3.9/site-packages/PyInstaller/hooks/rthooks/pyi_rth_subprocess.py'
4903 INFO: Including run-time hook '/home/martin/.local/lib/python3.9/site-packages/PyInstaller/hooks/rthooks/pyi_rth_pkgutil.py'
4905 INFO: Including run-time hook '/home/martin/.local/lib/python3.9/site-packages/PyInstaller/hooks/rthooks/pyi_rth_multiprocessing.py'
4910 INFO: Including run-time hook '/home/martin/.local/lib/python3.9/site-packages/PyInstaller/hooks/rthooks/pyi_rth_inspect.py'
4912 INFO: Including run-time hook '/home/martin/.local/lib/python3.9/site-packages/PyInstaller/hooks/rthooks/pyi_rth_pyqt5.py'
4916 INFO: Looking for dynamic libraries
6561 INFO: Looking for eggs
6561 INFO: Python library not in binary dependencies. Doing additional searching...
6596 INFO: Using Python library /lib/x86_64-linux-gnu/libpython3.9.so.1.0
6604 INFO: Warnings written to /home/martin/pyinstaller/linux2/no-datas/pyqt5/build/app/warn-app.txt
6625 INFO: Graph cross-reference written to /home/martin/pyinstaller/linux2/no-datas/pyqt5/build/app/xref-app.html
6643 INFO: checking PYZ
6645 INFO: Building because name changed
6645 INFO: Building PYZ (ZlibArchive) /home/martin/pyinstaller/linux2/no-datas/pyqt5/build/app/PYZ-00.pyz
6923 INFO: Building PYZ (ZlibArchive) /home/martin/pyinstaller/linux2/no-datas/pyqt5/build/app/PYZ-00.pyz completed successfully.
6926 INFO: checking PKG
6926 INFO: Building because name changed
6927 INFO: Building PKG (CArchive) app.pkg
6959 INFO: Building PKG (CArchive) app.pkg completed successfully.
6962 INFO: Bootloader /home/martin/.local/lib/python3.9/site-packages/PyInstaller/bootloader/Linux-64bit-intel/run
6963 INFO: checking EXE
6963 INFO: Building because name changed
6964 INFO: Building EXE from EXE-00.toc
6969 INFO: Copying bootloader EXE to /home/martin/pyinstaller/linux2/no-datas/pyqt5/build/app/app
6970 INFO: Appending PKG archive to custom ELF section in EXE
6979 INFO: Building EXE from EXE-00.toc completed successfully.
6981 INFO: checking COLLECT
6982 INFO: Building COLLECT COLLECT-00.toc
8674 INFO: Building COLLECT COLLECT-00.toc completed successfully.

If you look in your folder you'll notice you now have two new folders dist and build.

build & dist folders created by PyInstaller build & dist folders created by PyInstaller

Below is a truncated listing of the folder content, showing the build and dist folders.

bash
.
&boxvr&boxh&boxh app.py
&boxvr&boxh&boxh app.spec
&boxvr&boxh&boxh build
&boxv   &boxur&boxh&boxh app
&boxv       &boxvr&boxh&boxh localpycos
&boxv       &boxvr&boxh&boxh Analysis-00.toc
&boxv       &boxvr&boxh&boxh COLLECT-00.toc
&boxv       &boxvr&boxh&boxh EXE-00.toc
&boxv       &boxvr&boxh&boxh PKG-00.pkg
&boxv       &boxvr&boxh&boxh PKG-00.toc
&boxv       &boxvr&boxh&boxh PYZ-00.pyz
&boxv       &boxvr&boxh&boxh PYZ-00.toc
&boxv       &boxvr&boxh&boxh app
&boxv       &boxvr&boxh&boxh app.pkg
&boxv       &boxvr&boxh&boxh base_library.zip
&boxv       &boxvr&boxh&boxh warn-app.txt
&boxv       &boxur&boxh&boxh xref-app.html
&boxur&boxh&boxh dist
    &boxvr&boxh&boxh app
    &boxv   &boxvr&boxh&boxh lib-dynload
    &boxv   &boxvr&boxh&boxh PyQt5
    &boxv   ...
    &boxv   &boxvr&boxh&boxh app
    &boxv   &boxur&boxh&boxh libQt5Core.so.5
    &boxur&boxh&boxh app.app

The build folder is used by PyInstaller to collect and prepare the files for bundling, it contains the results of analysis and some additional logs. For the most part, you can ignore the contents of this folder, unless you're trying to debug issues.

The dist (for "distribution") folder contains the files to be distributed. This includes your application, bundled as an executable file, together with any associated libraries (for example PyQt5) and binary .so files.

Everything necessary to run your application will be in this folder, meaning you can take this folder and "distribute" it to someone else to run your app.

You can try running your app yourself now, by running the executable file, named app from the dist folder. After a short delay you'll see the familiar window of your application pop up as shown below.

Simple app, running after being packaged Simple app, running after being packaged

In the same folder as your Python file, alongside the build and dist folders PyInstaller will have also created a .spec file. In the next section we'll take a look at this file, what it is and what it does.

The Spec file

The .spec file contains the build configuration and instructions that PyInstaller uses to package up your application. Every PyInstaller project has a .spec file, which is generated based on the command line options you pass when running pyinstaller.

When we ran pyinstaller with our script, we didn't pass in anything other than the name of our Python application file. This means our spec file currently contains only the default configuration. If you open it, you'll see something similar to what we have below.

python
# -*- mode: python ; coding: utf-8 -*-

block_cipher = None

a = Analysis(['app.py'],
             pathex=[],
             binaries=[],
             datas=[],
             hiddenimports=[],
             hookspath=[],
             hooksconfig={},
             runtime_hooks=[],
             excludes=[],
             win_no_prefer_redirects=False,
             win_private_assemblies=False,
             cipher=block_cipher,
             noarchive=False)

pyz = PYZ(a.pure, a.zipped_data,
             cipher=block_cipher)

exe = EXE(pyz,
          a.scripts,
          [],
          exclude_binaries=True,
          name='app',
          debug=False,
          bootloader_ignore_signals=False,
          strip=False,
          upx=True,
          console=True,
          disable_windowed_traceback=False,
          target_arch=None,
          codesign_identity=None,
          entitlements_file=None )

coll = COLLECT(exe,
               a.binaries,
               a.zipfiles,
               a.datas,
               strip=False,
               upx=True,
               upx_exclude=[],
               name='app')

The first thing to notice is that this is a Python file, meaning you can edit it and use Python code to calculate values for the settings. This is mostly useful for complex builds, for example when you are targeting different platforms and want to conditionally define additional libraries or dependencies to bundle.

Once a .spec file has been generated, you can pass this to pyinstaller instead of your script to repeat the previous build process. Run this now to rebuild your executable.

bash
pyinstaller app.spec

The resulting build will be identical to the build used to generate the .spec file (assuming you have made no changes). For many PyInstaller configuration changes you have the option of passing command-line arguments, or modifying your existing .spec file. Which you choose is up to you.

Tweaking the build

So far we've created a simple first build of a very basic application. Now we'll look at a few things we can do to tweak our build.

Naming your app

One of the simplest changes you can make is to provide a proper "name" for your application. By default the app takes the name of your source file (minus the extension), for example main or app. This isn't usually what you want.

You can provide a nicer name for PyInstaller to use for your executable file (and dist folder) by editing the .spec file to add a name= under the EXE and COLLECT blocks. On Linux you will want to use a name with no spaces (use hyphens instead).

python
exe = EXE(pyz,
          a.scripts,
          [],
          exclude_binaries=True,
          name='hello-world',
          debug=False,
          bootloader_ignore_signals=False,
          strip=False,
          upx=True,
          console=False
         )
coll = COLLECT(exe,
               a.binaries,
               a.zipfiles,
               a.datas,
               strip=False,
               upx=True,
               upx_exclude=[],
               name='hello-world')

The name under EXE is the name of the executable file, the name under COLLECT is the name of the output folder. Usually you would want these to be the same.

Alternatively, you can re-run the pyinstaller command and pass the -n or --name configuration flag along with your app.py script.

bash
pyinstaller -n "hello-world" app.py
# or
pyinstaller --name "hello-world" app.py

The resulting executable file will be given the name hello-world and the unpacked build placed in the folder dist\hello-world\. The name of the .spec file is taken from the name passed in on the command line, so this will also create a new spec file for you, called hello-world.spec in your root folder.

If you've created a new .spec delete the old one to avoid getting confused!

Application with custom name "hello-world" Application with custom name "hello-world"

Application icon

One simple improvement we can make is to change the application icon which is shown while the application is running. We can set this icon in the code directly. To show an icon on our window we need to modify our simple application a little bit, to add a call to .setWindowIcon().

python
from PyQt5 import QtWidgets, QtGui
import sys

class MainWindow(QtWidgets.QMainWindow):

    def __init__(self):
        super().__init__()

        self.setWindowTitle("Hello World")
        l = QtWidgets.QLabel("My simple app.")
        l.setMargin(10)
        self.setCentralWidget(l)

        self.show()

if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    app.setWindowIcon(QtGui.QIcon('penguin.svg'))
    w = MainWindow()
    app.exec()

Here we've added the .setWindowIcon call to the app instance. This defines a default icon to be used for all windows of our application. You can override this on a per-window basis if you like, by calling .setWindowIcon on the window itself.

If you run the above application you should now see the icon appears on the dock.

Window showing the custom penguin icon Window showing the custom penguin icon

You can use a PNG file instead of SVG, but if using PNG make sure that the icon is large enough not to appear blurry due to scaling.

Even if you don't see the icon, keep reading!

Dealing with relative paths

There is a gotcha here, which might not be immediately apparent. To demonstrate it, open up a shell and change to the folder where our script is located. Run it with

bash
python3 app.py

If the icons are in the correct location, you should see them. Now change to the parent folder, and try and run your script again (change <folder> to the name of the folder your script is in).

bash
cd ..
python3 <folder>/app.py

Window with icon missing Window with icon missing.

The icons don't appear. What's happening?

We're using relative paths to refer to our data files. These paths are relative to the current working directory -- not the folder your script is in. So if you run the script from elsewhere it won't be able to find the files.

One common reason for icons not to show up, is running examples in an IDE which uses the project root as the current working directory.

This is a minor issue before the app is packaged, but once it's installed you don't know what the current working directory will be when it is run -- if it's wrong your app won't be able to find anything. We need to fix this before we go any further, which we can do by making our paths relative to our application folder.

In the updated code below, we define a new variable basedir, using os.path.dirname to get the containing folder of __file__ which holds the full path of the current Python file. We then use this to build the relative paths for icons using os.path.join().

Since our app.py file is in the root of our folder, all other paths are relative to that.

python
from PyQt5 import QtWidgets, QtGui
import sys, os

basedir = os.path.dirname(__file__)


class MainWindow(QtWidgets.QMainWindow):

    def __init__(self):
        super().__init__()

        self.setWindowTitle("Hello World")
        l = QtWidgets.QLabel("My simple app.")
        l.setMargin(10)
        self.setCentralWidget(l)
        self.show()

if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    app.setWindowIcon(QtGui.QIcon(os.path.join(basedir, 'penguin.svg')))
    w = MainWindow()
    app.exec_()

Try and run your app again from the parent folder -- you'll find that the icon now appear as expected, no matter where you launch the app from.

With this added to your script, running it should now show the icon on your window and taskbar. The final step is to ensure that this icon is correctly packaged with your application and continues to be shown when run from the dist folder.

Try it, it wont.

The issue is that our application now has a dependency on a external data file (the icon file) that's not part of our source. For our application to work, we now need to distribute this data file along with it. PyInstaller can do this for us, but we need to tell it what we want to include, and where to put it in the output.

In the next section we'll look at the options available to you for managing data files associated with your app.

Data files and Resources

So far we successfully built a simple app which had no external dependencies. However, once we needed to load an external file (in this case an icon) we hit upon a problem. The file wasn't copied into our dist folder and so could not be loaded.

In this section we'll look at the options we have to be able to bundle external resources, such as icons or Qt Designer .ui files, with our applications.

Bundling data files with PyInstaller

The simplest way to get these data files into the dist folder is to just tell PyInstaller to copy them over. PyInstaller accepts a list of individual file paths to copy over, together with a folder path relative to the dist/<app name> folder where it should to copy them to.

As with other options, this can be specified by command line arguments, --add-data

bash
pyinstaller --add-data "penguin.svg:." --name "hello-world" app.py

You can provide `--add-data` multiple times. Note that the path separator is platform-specific, on Linux or Mac use `:` while on Windows use `;`

Or via the datas list in the Analysis section of the spec file, shown below.

python
a = Analysis(['app.py'],
             pathex=[],
             binaries=[],
             datas=[('penguin.svg', '.')],
             hiddenimports=[],
             hookspath=[],
             runtime_hooks=[],
             excludes=[],
             win_no_prefer_redirects=False,
             win_private_assemblies=False,
             cipher=block_cipher,
             noarchive=False)

And then execute the .spec file with

bash
pyinstaller hello-world.spec

In both cases we are telling PyInstaller to copy the specified file penguin.svg to the location . which means the output folder dist. We could specify other locations here if we wanted. On the command line the source and destination are separated by the path separator :, whereas in the .spec file, the values are provided as a 2-tuple of strings.

If you run the build, you should see your .svg file now in the output folder dist ready to be distributed with your application.

The icon file copied to the dist folder The icon file copied to the dist folder

If you run your app from dist you should now see the icon on the window, and on the taskbar as expected.

The penguin icon showing on the dock The penguin icon showing on the dock

The file must be loaded in Qt using a relative path, and be in the same relative location to the EXE as it was to the .py file for this to work.

Bundling data folders

Usually you will have more than one data file you want to include with your packaged file. The latest PyInstaller versions let you bundle folders just like you would files, keeping the sub-folder structure. For example, lets extend our app to add some additional icons, and put them under a folder.

python
from PyQt5.QtWidgets import QMainWindow, QApplication, QLabel, QVBoxLayout, QPushButton, QWidget
from PyQt5.QtGui import QIcon
import sys, os

basedir = os.path.dirname(__file__)


class MainWindow(QMainWindow):

    def __init__(self):
        super().__init__()

        self.setWindowTitle("Hello World")
        layout = QVBoxLayout()
        label = QLabel("My simple app.")
        label.setMargin(10)
        layout.addWidget(label)

        button = QPushButton("Push")
        button.setIcon(QIcon(os.path.join(basedir, "icons", "lightning.svg")))
        button.pressed.connect(self.close)
        layout.addWidget(button)

        container = QWidget()
        container.setLayout(layout)

        self.setCentralWidget(container)

        self.show()

if __name__ == '__main__':
    app = QApplication(sys.argv)
    app.setWindowIcon(QIcon(os.path.join(basedir, "icons", "penguin.svg")))
    w = MainWindow()
    app.exec_()

The icons (both SVG files) are stored under a subfolder named 'icons'.

bash
.
&boxvr&boxh&boxh app.py
&boxur&boxh&boxh icons
    &boxur&boxh&boxh lightning.svg
    &boxur&boxh&boxh penguin.svg

If you run this you'll see the following window, with an icon on the button and an icon in the dock.

Two icons Window with two icons, and a button.

The paths are using the Unix forward-slash / convention, so they are cross-platform for macOS. If you're only developing for Windows, you can use \\

To copy the icons folder across to our build application, we just need to add the folder to our .spec file Analysis block. As for the single file, we add it as a tuple with the source path (from our project folder) and the destination folder under the resulting dist folder.

python
# -*- mode: python ; coding: utf-8 -*-


block_cipher = None


a = Analysis(['app.py'],
             pathex=[],
             binaries=[],
             datas=[('icons', 'icons')],   # tuple is (source_folder, destination_folder)
             hiddenimports=[],
             hookspath=[],
             hooksconfig={},
             runtime_hooks=[],
             excludes=[],
             win_no_prefer_redirects=False,
             win_private_assemblies=False,
             cipher=block_cipher,
             noarchive=False)
pyz = PYZ(a.pure, a.zipped_data,
             cipher=block_cipher)

exe = EXE(pyz,
          a.scripts,
          [],
          exclude_binaries=True,
          name='hello-world',
          debug=False,
          bootloader_ignore_signals=False,
          strip=False,
          upx=True,
          console=False,
          disable_windowed_traceback=False,
          target_arch=None,
          codesign_identity=None,
          entitlements_file=None )
coll = COLLECT(exe,
               a.binaries,
               a.zipfiles,
               a.datas,
               strip=False,
               upx=True,
               upx_exclude=[],
               name='hello-world')


If you run the build using this spec file you'll see the icons folder copied across to the dist folder. If you run the application from the folder, the icons will display as expected -- the relative paths remain correct in the new location.

Alternatively, you can bundle your data files using Qt's QResource architecture. See our tutorial for more information.

Creating a Linux Package (Ubuntu deb)

So far we've used PyInstaller to bundle the application into a Linux executable, along with the associated data files. The output of this bundling process is a folder. However, in order to share this application with other people and allow them to install it, we need to create a Linux package. Packages are distributable files which allow users to install software on their Linux system, as well as setting up things like application entries in the dock/menu.

On Ubuntu (and Debian) packages are named .deb files, on Redhat .rpm and on Arch Linux .pacman. These files are all different formats, but thankfully the process for building them is the same: using a tool named fpm.

In this tutorial we'll work through the steps for creating a Linux package, using an Ubuntu .deb file as an example. However, you will be able to use the same steps for your own system.

Installing fpm

The fpm tool is written in ruby and requires ruby to be installed to use it. Install ruby using your systems package manager, for example.

bash
$ sudo apt install ruby

Once ruby is installed, you can install fpm using the gem tool.

bash
$ gem install fpm --user-install

If you see a warning e.g. You don't have /home/martin/.local/share/gem/ruby/2.7.0/bin in your PATH you will need to add that to your path in your .bashrc file.

...and that's it. Once the installation is complete, you're ready to use fpm. You can check it is installed and working by running:

bash
$ fpm --version
1.14.2

Checking your build

In a terminal, change to the folder containing your application source files & run a PyInstaller build to generate the dist folder. Test that the generated build runs as expected (it works, and icons appear) by opening the dist folder in the file manager, and double-clicking on the application executable.

If everything works, you're ready to package the application -- if not, go back and double check everything.

It's always a good idea to test your built application before packaging it. Then if anything goes wrong, you know where the problem is!

Now let's package our folder using fpm.

Structuring your package

Linux files are used to install all sorts of applications, including system tools. Because of this they are set up to allow you to place files anywhere in the Linux filesystem -- and there are specific correct places to put different files. For a bundled package like ours, we can -- thankfully -- put our executable and associated data files all under the same folder (in /opt). However, to have our application show up in the menus/search we'll also need to install a .desktop file under /usr/share/applications.

The simplest way to ensure things end up in the correct location is to recreate the target file structure in a folder & then tell fpm to package using that folder as the root. This process is also easily automatable using a script (see later).

In your projects root folder, create a new folder called package and subfolders which map to the target filesystem -- /opt will hold our application folder hello-world, and /usr/share/applications will hold our .desktop file., while /usr/share/icons... will hold our application icon.

bash
$ mkdir -p package/opt
$ mkdir -p package/usr/share/applications
$ mkdir -p package/usr/share/icons/hicolor/scalable/apps

Next copy (recursively, with -r to include subfolders) the contents of dist/app to package/opt/hello-world -- the /opt/hello-world path is the destination of our application folder after installation.

bash
$ cp -r dist/hello-world package/opt/hello-world

We're copying the dist/hello-world folder. The name of this folder will depend on the application name configured in PyInstaller.

The icons

We've already set an icon for our application while it's running, using the penguin.svg file. However, we want our application to show it's icon in the dock/menus. To do this correctly, we need to copy our application icons into a specific location, under /usr/share/icons.

This folder contains all the icon themes installed on the system, but default icons for applications are always placed in the fallback hicolor theme, at /usr/share/icons/hicolor. Inside this folder, there are various folders for different sizes of icons.

bash
$ ls /usr/share/icons/hicolor/
128x128/          256x256/          64x64/            scalable/
16x16/            32x32/            72x72/            symbolic/
192x192/          36x36/            96x96/
22x22/            48x48/            icon-theme.cache
24x24/            512x512/          index.theme

We're using the scalable folder, since our icon is an SVG (Scalable Vector Graphics). If you're using a specifically sized PNG file, place it in the correct location -- and feel free to add multiple different sizes, to ensure your application icon looks good when scaled. Application icons go in the subfolder apps.

bash
$ cp icons/penguin.svg package/usr/share/icons/hicolor/scalable/apps/hello-world.svg

IMPORTANT: Name the destination filename of the icon after your application to avoid it clashing with any others! Here we're calling it hello-world.svg.

The .desktop file

The .desktop file is a text configuration file which tells the Linux desktop about a desktop application -- for example, where to fine the executable, the name and which icon to display. You should include a .desktop file for your apps to make them easy to use. An example .desktop file is shown below -- add this to the root folder of your project -- with the name hello-world.desktop, and make any changes you like.

ini
[Desktop Entry]

# The type of the thing this desktop file refers to (e.g. can be Link)
Type=Application

# The application name.
Name=Hello World

# Tooltip comment to show in menus.
Comment=A simple Hello World application.

# The path (folder) in which the executable is run
Path=/opt/hello-world

# The executable (can include arguments)
Exec=/opt/hello-world/hello-world

# The icon for the entry, using the name from `hicolor/scalable` without the extension.
# You can also use a full path to a file in /opt.
Icon=hello-world


For more information on creating .desktop files see this documentation.

Now the hello-world.desktop file is ready, we can copy it into our install package with.

bash
$ cp hello-world.desktop package/usr/share/applications

Permissions

Packages retain the permissions of installed files from when they were packaged, but will be installed by root. In order for ordinary users to be able to run the application, you need to change the permissions of the files created.

We can recursively apply the correct permissions 755 - owner can read/write/execute, group/others can read/execute. to our executable and folders, and 644, owner can read/write, group/others can read to all our other library and icons/desktop files.

bash
find package/opt/hello-world -type f -exec chmod 644 -- {} +
find package/opt/hello-world -type d -exec chmod 755 -- {} +
find package/usr/share -type f -exec chmod 644 -- {} +
chmod +x package/opt/hello-world/hello-world

Building your package

Now everything is where it should be in our package "filesystem", we're ready to start building the package itself.

Enter the following into your shell.

bash
fpm -C package -s dir -t deb -n "hello-world" -v 0.1.0 -p hello-world.deb

The arguments in order are:

  • -C the folder to change to before searching for files: our package folder
  • -s the type of source(s) to package: in our case dir, a folder
  • -t the type of package to build: a deb Debian/Ubuntu package
  • -n the name of the application: "hello-world"
  • -v the version of the application: 0.1.0
  • -p the package name to output: hello-world-deb

For more command line arguments, see the fpm documentation.

You can create other package types (for other Linux distributions) by changing the -t argument.

After a few seconds, you should see a message to indicate that the package has been created.

bash
$ fpm -C package -s dir -t deb -n "hello-world" -v 0.1.0 -p hello-world.deb
Created package {:path=>"hello-world.deb"}

Installation

The package is ready! Let's install it.

bash
$ sudo dpkg -i hello-world.deb

You'll see some output as the install completes.

python
Selecting previously unselected package hello-world.
(Reading database ... 172208 files and directories currently installed.)
Preparing to unpack hello-world.deb ...
Unpacking hello-world (0.1.0) ...
Setting up hello-world (0.1.0) ...

Once installation has completed, you can check the files are where you expect, under /opt/hello-world

bash
$ ls /opt/hello-world
app                        libpcre2-8.so.0
base_library.zip           libpcre.so.3
icons                      libpixman-1.so.0
libatk-1.0.so.0            libpng16.so.16
libatk-bridge-2.0.so.0     libpython3.9.so.1.0
etc.

Next try and run the application from the menu/dock -- you can search for "Hello World" and the application will be found (thanks to the .desktop file).

Hello world in Ubuntu search Application shows up in the Ubuntu search panel, and will also appear in menus on other environments.

If you run the application, the icons will show up as expected.

Application, running in the dock Application runs and all icons show up as expected.

Scripting the build

We've walked through the steps required to build an installable Ubuntu .deb package from a PyQt5 application. There isn't that much too it, but if you have to do it more than once it'll quickly get quite tedious and prone to mistakes. To avoid problems I recommend scripting this with a simple bash script & fpm own automation tool.

In this section I'll give you scripts that automate the build we've done for our Hello World application.

package.sh

Save in your project root and chmod +x to make it executable.

sh
#!/bin/sh
# Create folders.
[ -e package ] && rm -r package
mkdir -p package/opt
mkdir -p package/usr/share/applications
mkdir -p package/usr/share/icons/hicolor/scalable/apps

# Copy files (change icon names, add lines for non-scaled icons)
cp -r dist/hello-world package/opt/hello-world
cp icons/penguin.svg package/usr/share/icons/hicolor/scalable/apps/hello-world.svg
cp hello-world.desktop package/usr/share/applications

# Change permissions
find package/opt/hello-world -type f -exec chmod 644 -- {} +
find package/opt/hello-world -type d -exec chmod 755 -- {} +
find package/usr/share -type f -exec chmod 644 -- {} +
chmod +x package/opt/hello-world/hello-world

.fpm file

fpm allows you to store the configuration for the packaging in a configuration file. The file name must be .fpm and it must be in the folder you run the fpm tool. Our configuration is as follows.

sh
-C package
-s dir
-t deb
-n "hello-world"
-v 0.1.0
-p hello-world.deb

You can override any of the options you like when executing fpm by passing command line arguments as normal.

Executing the build

With these scripts in place our application can be packaged reproducibly with the commands:

bash
pyinstaller hello-world.spec
./package.sh
fpm

Feel free to customize these build scripts further yourself to suit your own project!

Wrapping up

In this tutorial we've covered how to build your PyQt5 applications into a Linux executable using PyInstaller, including adding data files along with your code. Then we walked through the process of creating a Ubuntu .deb package to distribute your app to others. Following these steps you should be able to package up your own applications and make them available to other people.

Packaging Tkinter applications for Windows with PyInstaller & InstallForge

There is not much fun in creating your own desktop applications if you can't share them with other people — whether than means publishing it commercially, sharing it online or just giving it to someone you know. Sharing your apps allows other people to benefit from your hard work!

The good news is there are tools available to help you do just that with your Python applications which work well with apps built using Tkinter. In this tutorial we'll look at the most popular tool for packaging Python applications: PyInstaller.

This tutorial is broken down into a series of steps, using PyInstaller to build first simple, and then increasingly complex Tkinter applications into distributable EXE files on Windows. You can choose to follow it through completely, or skip ahead to the examples that are most relevant to your own project.

We finish off by using InstallForge to create a distributable Windows installer.

You always need to compile your app on your target system. So, if you want to create a Mac .app you need to do this on a Mac, for an EXE you need to use Windows.

Example Installer for Windows Example Installer for Windows

If you're impatient, you can download the Example Installer for Windows first.

Requirements

PyInstaller works out of the box with Tkinter and as of writing, current versions of PyInstaller are compatible with Python 3.6+. Whatever project you're working on, you should be able to package your apps.

You can install PyInstaller using pip.

bash
pip3 install PyInstaller

If you experience problems packaging your apps, your first step should always be to update your PyInstaller and hooks package the latest versions using

bash
pip3 install --upgrade PyInstaller pyinstaller-hooks-contrib

The hooks module contains package-specific packaging instructions for PyInstaller which is updated regularly.

Install in virtual environment (optional)

You can also opt to install PyInstaller in a virtual environment (or your applications virtual environment) to keep your environment clean.

bash
python3 -m venv packenv

Once created, activate the virtual environment by running from the command line —

bash
call packenv\scripts\activate.bat

Finally, install the required libraries.

python
pip3 install PyInstaller

Getting Started

It's a good idea to start packaging your application from the very beginning so you can confirm that packaging is still working as you develop it. This is particularly important if you add additional dependencies. If you only think about packaging at the end, it can be difficult to debug exactly where the problems are.

For this example we're going to start with a simple skeleton app, which doesn't do anything interesting. Once we've got the basic packaging process working, we'll extend the application to include icons and data files. We'll confirm the build as we go along.

To start with, create a new folder for your application and then add the following skeleton app in a file named app.py. You can also download the source code and associated files

python
import tkinter as tk

window = tk.Tk()
window.title("Hello World")


def handle_button_press(event):
    window.destroy()


button = tk.Button(text="My simple app.")
button.bind("<Button-1>", handle_button_press)
button.pack()

# Start the event loop.
window.mainloop()

This is a basic bare-bones application which creates a window and adds a simple button to it. You can run this app as follows.

bash
python app.py

This should produce the following window (on Windows 11).

Simple skeleton app in Tkinter Simple skeleton app in Tkinter

Building a basic app

Now we have our simple application skeleton in place, we can run our first build test to make sure everything is working.

Open your terminal (command prompt) and navigate to the folder containing your project. You can now run the following command to run the PyInstaller build.

python
pyinstaller app.py

You'll see a number of messages output, giving debug information about what PyInstaller is doing. These are useful for debugging issues in your build, but can otherwise be ignored. The output that I get for running the command on Windows 11 is shown below.

bash
C:\Users\Martin\pyinstaller\tkinter\basic>pyinstaller app.py
335 INFO: PyInstaller: 4.7
335 INFO: Python: 3.7.6
336 INFO: Platform: Windows-10-10.0.22000-SP0
337 INFO: wrote C:\Users\Martin\pyinstaller\tkinter\basic\app.spec
339 INFO: UPX is not available.
344 INFO: Extending PYTHONPATH with paths
['C:\\Users\\Martin\\pyinstaller\\tkinter\\basic']
1923 INFO: checking Analysis
1923 INFO: Building Analysis because Analysis-00.toc is non existent
1924 INFO: Initializing module dependency graph...
1928 INFO: Caching module graph hooks...
1951 INFO: Analyzing base_library.zip ...
7438 INFO: Caching module dependency graph...
7604 INFO: running Analysis Analysis-00.toc
7620 INFO: Adding Microsoft.Windows.Common-Controls to dependent assemblies of final executable
  required by c:\users\gebruiker\appdata\local\programs\python\python37\python.exe
8188 INFO: Analyzing C:\Users\Martin\pyinstaller\tkinter\basic\app.py
8377 INFO: Processing module hooks...
8378 INFO: Loading module hook 'hook-difflib.py' from 'c:\\users\\gebruiker\\appdata\\local\\programs\\python\\python37\\lib\\site-packages\\PyInstaller\\hooks'...
8380 INFO: Loading module hook 'hook-encodings.py' from 'c:\\users\\gebruiker\\appdata\\local\\programs\\python\\python37\\lib\\site-packages\\PyInstaller\\hooks'...
8448 INFO: Loading module hook 'hook-heapq.py' from 'c:\\users\\gebruiker\\appdata\\local\\programs\\python\\python37\\lib\\site-packages\\PyInstaller\\hooks'...
8450 INFO: Loading module hook 'hook-pickle.py' from 'c:\\users\\gebruiker\\appdata\\local\\programs\\python\\python37\\lib\\site-packages\\PyInstaller\\hooks'...
8452 INFO: Loading module hook 'hook-xml.py' from 'c:\\users\\gebruiker\\appdata\\local\\programs\\python\\python37\\lib\\site-packages\\PyInstaller\\hooks'...
8765 INFO: Loading module hook 'hook-_tkinter.py' from 'c:\\users\\gebruiker\\appdata\\local\\programs\\python\\python37\\lib\\site-packages\\PyInstaller\\hooks'...
8888 INFO: checking Tree
8888 INFO: Building Tree because Tree-00.toc is non existent
8889 INFO: Building Tree Tree-00.toc
8959 INFO: checking Tree
8959 INFO: Building Tree because Tree-01.toc is non existent
8960 INFO: Building Tree Tree-01.toc
9036 INFO: checking Tree
9036 INFO: Building Tree because Tree-02.toc is non existent
9037 INFO: Building Tree Tree-02.toc
9058 INFO: Looking for ctypes DLLs
9063 INFO: Analyzing run-time hooks ...
9065 INFO: Including run-time hook 'c:\\users\\gebruiker\\appdata\\local\\programs\\python\\python37\\lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_pkgutil.py'
9073 INFO: Including run-time hook 'c:\\users\\gebruiker\\appdata\\local\\programs\\python\\python37\\lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_win32api.py'
9117 INFO: Processing pre-find module path hook distutils from 'c:\\users\\gebruiker\\appdata\\local\\programs\\python\\python37\\lib\\site-packages\\PyInstaller\\hooks\\pre_find_module_path\\hook-distutils.py'.
9118 INFO: distutils: retargeting to non-venv dir 'c:\\users\\gebruiker\\appdata\\local\\programs\\python\\python37\\lib'
9204 INFO: Including run-time hook 'c:\\users\\gebruiker\\appdata\\local\\programs\\python\\python37\\lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_inspect.py'
9209 INFO: Including run-time hook 'c:\\users\\gebruiker\\appdata\\local\\programs\\python\\python37\\lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth__tkinter.py'
9217 INFO: Looking for dynamic libraries
9434 INFO: Looking for eggs
9434 INFO: Using Python library c:\users\gebruiker\appdata\local\programs\python\python37\python37.dll
9435 INFO: Found binding redirects:
[]
9451 INFO: Warnings written to C:\Users\Martin\pyinstaller\tkinter\basic\build\app\warn-app.txt
9483 INFO: Graph cross-reference written to C:\Users\Martin\pyinstaller\tkinter\basic\build\app\xref-app.html
9516 INFO: checking PYZ
9517 INFO: Building PYZ because PYZ-00.toc is non existent
9517 INFO: Building PYZ (ZlibArchive) C:\Users\Martin\pyinstaller\tkinter\basic\build\app\PYZ-00.pyz
9978 INFO: Building PYZ (ZlibArchive) C:\Users\Martin\pyinstaller\tkinter\basic\build\app\PYZ-00.pyz completed successfully.
9991 INFO: checking PKG
9992 INFO: Building PKG because PKG-00.toc is non existent
9992 INFO: Building PKG (CArchive) app.pkg
10013 INFO: Building PKG (CArchive) app.pkg completed successfully.
10015 INFO: Bootloader c:\users\gebruiker\appdata\local\programs\python\python37\lib\site-packages\PyInstaller\bootloader\Windows-64bit\run.exe
10015 INFO: checking EXE
10015 INFO: Building EXE because EXE-00.toc is non existent
10015 INFO: Building EXE from EXE-00.toc
10015 INFO: Copying bootloader EXE to C:\Users\Martin\pyinstaller\tkinter\basic\build\app\app.exe
10077 INFO: Copying icon to EXE
10077 INFO: Copying icons from ['c:\\users\\gebruiker\\appdata\\local\\programs\\python\\python37\\lib\\site-packages\\PyInstaller\\bootloader\\images\\icon-console.ico']
10141 INFO: Writing RT_GROUP_ICON 0 resource with 104 bytes
10141 INFO: Writing RT_ICON 1 resource with 3752 bytes
10141 INFO: Writing RT_ICON 2 resource with 2216 bytes
10142 INFO: Writing RT_ICON 3 resource with 1384 bytes
10142 INFO: Writing RT_ICON 4 resource with 37019 bytes
10143 INFO: Writing RT_ICON 5 resource with 9640 bytes
10143 INFO: Writing RT_ICON 6 resource with 4264 bytes
10143 INFO: Writing RT_ICON 7 resource with 1128 bytes
10146 INFO: Copying 0 resources to EXE
10146 INFO: Emedding manifest in EXE
10147 INFO: Updating manifest in C:\Users\Martin\pyinstaller\tkinter\basic\build\app\app.exe
10206 INFO: Updating resource type 24 name 1 language 0
10209 INFO: Appending PKG archive to EXE
10739 INFO: Building EXE from EXE-00.toc completed successfully.
10743 INFO: checking COLLECT
10743 INFO: Building COLLECT because COLLECT-00.toc is non existent
10744 INFO: Building COLLECT COLLECT-00.toc
15439 INFO: Building COLLECT COLLECT-00.toc completed successfully.


If you look in your folder you'll notice you now have two new folders dist and build.

build & dist folders created by PyInstaller build & dist folders created by PyInstaller

Below is a truncated listing of the folder content, showing the build and dist folders.

bash
.
&boxvr&boxh&boxh app.py
&boxvr&boxh&boxh app.spec
&boxvr&boxh&boxh build
&boxv   &boxur&boxh&boxh app
&boxv       &boxvr&boxh&boxh Analysis-00.toc
&boxv       &boxvr&boxh&boxh app.exe
&boxv       &boxvr&boxh&boxh app.exe.manifest
&boxv       &boxvr&boxh&boxh app.pkg
&boxv       &boxvr&boxh&boxh base_library.zip
&boxv       &boxvr&boxh&boxh COLLECT-00.toc
&boxv       &boxur&boxh&boxh EXE-00.toc
&boxur&boxh&boxh dist
    &boxur&boxh&boxh app
        &boxvr&boxh&boxh tcl
        &boxvr&boxh&boxh tcl8
        &boxvr&boxh&boxh tk
        &boxvr&boxh&boxh app.exe
        ...

The build folder is used by PyInstaller to collect and prepare the files for bundling, it contains the results of analysis and some additional logs. For the most part, you can ignore the contents of this folder, unless you're trying to debug issues.

The dist (for "distribution") folder contains the files to be distributed. This includes your application, bundled as an executable file, together with any associated libraries and binary .dll files.

Everything necessary to run your application will be in this folder, meaning you can take this folder and "distribute" it to someone else to run your app.

You can try running your app yourself now, by running the executable file, named app.exe from the dist folder. After a short delay you'll see the familiar window of your application pop up as shown below.

Simple app, running after being packaged Simple app, running after being packaged

You may also notice a console/terminal window pop up as your application runs. We'll cover how to stop that happening shortly.

In the same folder as your Python file, alongside the build and dist folders PyInstaller will have also created a .spec file. In the next section we'll take a look at this file, what it is and what it does.

The Spec file

The .spec file contains the build configuration and instructions that PyInstaller uses to package up your application. Every PyInstaller project has a .spec file, which is generated based on the command line options you pass when running pyinstaller.

When we ran pyinstaller with our script, we didn't pass in anything other than the name of our Python application file. This means our spec file currently contains only the default configuration. If you open it, you'll see something similar to what we have below.

python
# -*- mode: python ; coding: utf-8 -*-

block_cipher = None


a = Analysis(['app.py'],
             pathex=[],
             binaries=[],
             datas=[],
             hiddenimports=[],
             hookspath=[],
             runtime_hooks=[],
             excludes=[],
             win_no_prefer_redirects=False,
             win_private_assemblies=False,
             cipher=block_cipher,
             noarchive=False)
pyz = PYZ(a.pure, a.zipped_data,
             cipher=block_cipher)
exe = EXE(pyz,
          a.scripts,
          [],
          exclude_binaries=True,
          name='app',
          debug=False,
          bootloader_ignore_signals=False,
          strip=False,
          upx=True,
          console=True )
coll = COLLECT(exe,
               a.binaries,
               a.zipfiles,
               a.datas,
               strip=False,
               upx=True,
               upx_exclude=[],
               name='app')


The first thing to notice is that this is a Python file, meaning you can edit it and use Python code to calculate values for the settings. This is mostly useful for complex builds, for example when you are targeting different platforms and want to conditionally define additional libraries or dependencies to bundle.

If you generate a .spec file on Windows the path separator will be \\. To use this same .spec file on macOS you'll need to switch the separators to /. Thankfully / also works on Windows.

Once a .spec file has been generated, you can pass this to pyinstaller instead of your script to repeat the previous build process. Run this now to rebuild your executable.

bash
pyinstaller app.spec

The resulting build will be identical to the build used to generate the .spec file (assuming you have made no changes). For many PyInstaller configuration changes you have the option of passing command-line arguments, or modifying your existing .spec file. Which you choose is up to you.

Tweaking the build

So far we've created a simple first build of a very basic application. Now we'll look at a few of the most useful options that PyInstaller provides to tweak our build. Then we'll go on to look at building more complex applications.

Naming your app

One of the simplest changes you can make is to provide a proper "name" for your application. By default the app takes the name of your source file (minus the extension), for example main or app. This isn't usually what you want.

You can provide a nicer name for PyInstaller to use for the executable (and dist folder) either by editing the .spec file to add a name= under the app block.

python
exe = EXE(pyz,
          a.scripts,
          [],
          exclude_binaries=True,
          name='Hello World',
          debug=False,
          bootloader_ignore_signals=False,
          strip=False,
          upx=True,
          console=False  # False = do not show console.
         )

Alternatively, you can re-run the pyinstaller command and pass the -n or --name configuration flag along with your app.py script.

bash
pyinstaller -n "Hello World" app.py
# or
pyinstaller --name "Hello World" app.py

The resulting EXE file will be given the name Hello World.exe and placed in the folder dist\Hello World\.

Application with custom name "Hello World" Application with custom name "Hello World"

The name of the .spec file is taken from the name passed in on the command line, so this will also create a new spec file for you, called Hello World.spec in your root folder.

Hiding the console window

When you run your packaged application you will notice that a console window runs in the background. If you try and close this console window your application will also close. You almost never want this window in a GUI application and PyInstaller provides a simple way to turn this off.

Application running with terminal in background Application running with terminal in background

You can fix this in one of two ways. Firstly, you can edit the previously created .spec file setting console=False under the EXE block as shown below.

python
exe = EXE(pyz,
          a.scripts,
          [],
          exclude_binaries=True,
          name='app',
          debug=False,
          bootloader_ignore_signals=False,
          strip=False,
          upx=True,
          console=False  # False = do not show console.
         )

Alternatively, you can re-run the pyinstaller command and pass the -w, --noconsole or --windowed configuration flag along with your app.py script.

bash
pyinstaller -w app.py
# or
pyinstaller --windowed app.py
# or
pyinstaller --noconsole app.py


There is no difference between any of the options.

Re-running pyinstaller will re-generate the .spec file. If you've made any other changes to this file these will be lost.

One File Build

On Windows PyInstaller has the ability to create a one-file build, that is, a single EXE file which contains all your code, libraries and data files in one. This can be a convenient way to share simple applications, as you don't need to provide an installer or zip up a folder of files.

To specify a one-file build provide the --onefile flag at the command line.

bash
pyinstaller --onefile app.py

Note that while the one-file build is easier to distribute, it is slower to execute than a normally built application. This is because every time the application is run it must create a temporary folder to unpack the contents of the executable. Whether this trade-off is worth the convenience for your app is up to you!

Using the --onefile option makes quite a few changes to the .spec file. You can make these changes manually, but it's much simpler to use the command line switch when first creating your .spec

Since debugging a one file app is much harder, you should make sure everything is working with a normal build before you create a one-file package.

We're going to continue this tutorial with a folder-based build for clarity.

Setting an application Icon

By default PyInstaller EXE files come with the following icon in place.

Default PyInstaller application icon, on app.exe Default PyInstaller application icon, on app.exe

You will probably want to customize this to make your application more recognisable. This can be done easily using the --icon=<filename> command-line switch to PyInstaller. On Windows the icon should be provided as an .ico file.

bash
pyinstaller --windowed --icon=icon.ico app.py

The portable version of IcoFx is a good free tool to create icons on Windows.

Or, by adding the icon= parameter to your .spec file.

python
exe = EXE(pyz,
          a.scripts,
          [],
          exclude_binaries=True,
          name='blarh',
          debug=False,
          bootloader_ignore_signals=False,
          strip=False,
          upx=True,
          console=False,
          icon='icon.ico')

If you now re-run the build (by using the command line arguments, or running with your modified .spec file) you'll see the specified icon file is now set on your application's EXE file.

Custom application icon (a hand) on app.exe Custom application icon (a hand) on app.exe

However, if you run your application, you're going to be disappointed.

The custom EXE icon is not applied to the window The custom EXE icon is not applied to the window

The specified icon is not showing up on the window, and it will also not appear on your taskbar.

Why not? Because the icon used for the window isn't determined by the icons in the executable file, but by the application itself. To show an icon on our window we need to modify our simple application a little bit, to add a call to window.iconphoto().

python
import tkinter as tk

window = tk.Tk()
window.title("Hello World")


def handle_button_press(event):
    window.destroy()


button = tk.Button(text="My simple app.")
button.bind("<Button-1>", handle_button_press)
button.pack()

# Start the event loop.
window.iconbitmap("icon.ico")
window.mainloop()

Here we've added the .iconbitmap call to the window instance. This defines an icon to be used for our application's window.

If you run the above application you should now see the icon appears on the window.

Window showing the custom icon Window showing the custom icon

Even if you don't see the icons, keep reading!

Dealing with relative paths

There is a gotcha here, which might not be immediately apparent. To demonstrate it, open up a shell and change to the folder where our script is located. Run it with

bash
python3 app.py

If the icons are in the correct location, you should see them. Now change to the parent folder, and try and run your script again (change <folder> to the name of the folder your script is in).

bash
cd ..
python3 <folder>/app.py

Window with icon missing Window with icon missing.

The icons don't appear. What's happening?

We're using relative paths to refer to our data files. These paths are relative to the current working directory -- not the folder your script is in. So if you run the script from elsewhere it won't be able to find the files.

One common reason for icons not to show up, is running examples in an IDE which uses the project root as the current working directory.

This is a minor issue before the app is packaged, but once it's installed you don't know what the current working directory will be when it is run -- if it's wrong your app won't be able to find anything. We need to fix this before we go any further, which we can do by making our paths relative to our application folder.

In the updated code below, we define a new variable basedir, using os.path.dirname to get the containing folder of __file__ which holds the full path of the current Python file. We then use this to build the relative paths for icons using os.path.join().

Since our app.py file is in the root of our folder, all other paths are relative to that.

python
import os
import tkinter as tk

basedir = os.path.dirname(__file__)


window = tk.Tk()
window.title("Hello World")


def handle_button_press(event):
    window.destroy()


button_icon = tk.PhotoImage(file=os.path.join(basedir, "icon.png"))
button = tk.Button(text="My simple app.", image=button_icon)
button.bind("<Button-1>", handle_button_press)
button.pack()

# Set window icon.
window.iconbitmap(os.path.join(basedir, "icon.ico"))

window.mainloop()

Try and run your app again from the parent folder -- you'll find that the icon now appear as expected, no matter where you launch the app from.

Taskbar Icons

Unfortunately, even if the icon is showing on the window, it may still not show on the taskbar.

If it does for you, great! But it may not work when you distribute your application, so it's probably a good idea to follow the next steps anyway.

Custom icon is not shown on the toolbar Custom icon is not shown on the toolbar

The final tweak we need to make to get the icon showing on the taskbar is to add some cryptic incantations to the top of our Python file.

When you run your application, Windows looks at the executable and tries to guess what "application group" it belongs to. By default, any Python scripts (including your application) are grouped under the same "Python" group, and so will show the Python icon. To stop this happening, we need to provide Windows with a different application identifier.

The code below does this, by calling ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID() with a custom application id.

python
import os
import tkinter as tk

basedir = os.path.dirname(__file__)

try:
    from ctypes import windll  # Only exists on Windows.

    myappid = "mycompany.myproduct.subproduct.version"
    windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid)
except ImportError:
    pass

window = tk.Tk()
window.title("Hello World")


def handle_button_press(event):
    window.destroy()


button_icon = tk.PhotoImage(file=os.path.join(basedir, "icon.png"))
button = tk.Button(text="My simple app.", image=button_icon)

button.bind("<Button-1>", handle_button_press)
button.pack()

# Set window icon.
window.iconbitmap(os.path.join(basedir, "icon.ico"))

window.mainloop()

The listing above shows a generic mycompany.myproduct.subproduct.version string, but you should change this to reflect your actual application. It doesn't really matter what you put for this purpose, but the convention is to use reverse-domain notation, com.mycompany for the company identifier.

With this added to your script, running it should now show the icon on your window and taskbar. The final step is to ensure that this icon is correctly packaged with your application and continues to be shown when run from the dist folder.

Try it, it wont.

The issue is that our application now has a dependency on a external data file (the icon file) that's not part of our source. For our application to work, we now need to distribute this data file along with it. PyInstaller can do this for us, but we need to tell it what we want to include, and where to put it in the output.

In the next section we'll look at the options available to you for managing data files associated with your app.

Data files and Resources

So far we successfully built a simple app which had no external dependencies. However, once we needed to load an external file (in this case an icon) we hit upon a problem. The file wasn't copied into our dist folder and so could not be loaded.

In this section we'll look at the options we have to be able to bundle external resources, such as icons, with our applications.

Bundling data files with PyInstaller

The simplest way to get these data files into the dist folder is to just tell PyInstaller to copy them over. PyInstaller accepts a list of individual file paths to copy over, together with a folder path relative to the dist/<app name> folder where it should to copy them to.

As with other options, this can be specified by command line arguments, --add-data

bash
pyinstaller --windowed --icon=icon.ico --add-data="icon.ico;." app.py

You can provide `--add-data` multiple times. Note that the path separator is platform-specific, on Windows use `;` while on Linux or Mac use `:`

Or via the datas list in the Analysis section of the spec file, shown below.

python
a = Analysis(['app.py'],
             pathex=[],
             binaries=[],
             datas=[('icon.ico', '.')],
             hiddenimports=[],
             hookspath=[],
             runtime_hooks=[],
             excludes=[],
             win_no_prefer_redirects=False,
             win_private_assemblies=False,
             cipher=block_cipher,
             noarchive=False)

And then execute the .spec file with

bash
pyinstaller app.spec

In both cases we are telling PyInstaller to copy the specified file icon.ico to the location . which means the output folder dist. We could specify other locations here if we wanted. On the command line the source and destination are separated by the path separator ;, whereas in the .spec file, the values are provided as a 2-tuple of strings.

Run the build, and you will see your .ico file now in the output folder dist ready to be distributed with your application. If you run your app from dist you should now see the icon on the window, and on the taskbar as expected.

The hand icon showing on the toolbar The hand icon showing on the toolbar

The file must be loaded in your app using a relative path, and be in the same relative location to the EXE as it was to the .py file for this to work.

If your icon looks blurry it means you don't have large-enough icon variations in your .ico file. An .ico file can contain multiple different sized icons in the same file. Ideally you want to have 16x16, 32x32, 48x48 and 256x256 pixel sizes included, although fewer will still work.

Bundling data folders

Usually you will have more than one data file you want to include with your packaged file. The latest PyInstaller versions let you bundle folders just like you would files, keeping the sub-folder structure. For example, lets extend our app to add some additional icons, and put them under a folder.

python
import os
import tkinter as tk

basedir = os.path.dirname(__file__)

try:
    from ctypes import windll  # Only exists on Windows.

    myappid = "mycompany.myproduct.subproduct.version"
    windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid)
except ImportError:
    pass

window = tk.Tk()
window.title("Hello World")

label = tk.Label(text="My simple app.")
label.pack()


def handle_button_press(event):
    window.destroy()


button_close_icon = tk.PhotoImage(
    file=os.path.join(basedir, "icons", "lightning.png")
)
button_close = tk.Button(
    text="Close",
    image=button_close_icon,
)
button_close.bind("<Button-1>", handle_button_press)
button_close.pack()

button_maximimize_icon = tk.PhotoImage(
    file=os.path.join(basedir, "icons", "uparrow.png")
)
button_maximize = tk.Button(
    text="Maximize",
    image=button_maximimize_icon,
)
button_maximize.bind("<Button-1>", handle_button_press)
button_maximize.pack()

# Set window icon.
window.iconbitmap(os.path.join(basedir, "icons", "icon.ico"))

# Start the event loop.
window.mainloop()


The icons (PNG files and an ICO file for the Windows file icon) are stored under a subfolder named 'icons'.

bash
.
&boxvr&boxh&boxh app.py
&boxur&boxh&boxh icons
    &boxvr&boxh&boxh icon.png
    &boxvr&boxh&boxh icon.svg
    &boxvr&boxh&boxh lightning.png
    &boxvr&boxh&boxh lightning.svg
    &boxvr&boxh&boxh uparrow.png
    &boxvr&boxh&boxh uparrow.svg
    &boxur&boxh&boxh icon.ico

If you run this you'll see the following window, with a Window icon and a button icon.

Two icons Window with two buttons with icons.

The paths are using the Unix forward-slash / convention, so they are cross-platform for macOS. If you're only developing for Windows, you can use \\

To copy the icons folder across to our build application, we just need to add the folder to our .spec file Analysis block. As for the single file, we add it as a tuple with the source path (from our project folder) and the destination folder under the resulting dist folder.

python
# -*- mode: python ; coding: utf-8 -*-


block_cipher = None


a = Analysis(['app.py'],
             pathex=[],
             binaries=[],
             datas=[('icons', 'icons')],   # tuple is (source_folder, destination_folder)
             hiddenimports=[],
             hookspath=[],
             hooksconfig={},
             runtime_hooks=[],
             excludes=[],
             win_no_prefer_redirects=False,
             win_private_assemblies=False,
             cipher=block_cipher,
             noarchive=False)
pyz = PYZ(a.pure, a.zipped_data,
             cipher=block_cipher)

exe = EXE(pyz,
          a.scripts,
          [],
          exclude_binaries=True,
          name='app',
          debug=False,
          bootloader_ignore_signals=False,
          strip=False,
          upx=True,
          console=False,
          disable_windowed_traceback=False,
          target_arch=None,
          codesign_identity=None,
          entitlements_file=None )
coll = COLLECT(exe,
               a.binaries,
               a.zipfiles,
               a.datas,
               strip=False,
               upx=True,
               upx_exclude=[],
               name='app')


If you run the build using this spec file you'll see the icons folder copied across to the dist folder. If you run the application from the folder, the icons will display as expected -- the relative paths remain correct in the new location.

Building a Windows Installer with InstallForge

So far we've used PyInstaller to bundle applications for distribution, along with the associated data files. The output of this bundling process is a folder, named dist which contains all the files our application needs to run.

While you could share this folder with your users as a ZIP file it's not the best user experience. Desktop applications are normally distributed with installers which handle the process of putting the executable (and any other files) in the correct place, adding Start Menu shortcuts and the like.

Now we've successfully bundled our application, we'll next look at how we can take our dist folder and use it to create a Windows installer.

Making sure the build is ready.

If you've followed the tutorial so far, you'll already have your app ready in the /dist folder. If not, or yours isn't working you can also download the source code files for this tutorial which includes a sample .spec file. As above, you can run the same build using the provided app.spec file.

bash
pyinstaller app.spec

This packages everything up ready to distribute in the dist/app folder. Run the executable app.exe to ensure everything is bundled correctly, and you should the same window as before with icons visible.

Two icons Window with two icons, and a button.

The EXE section in the .spec has a name parameter where you can specify the name of the resulting EXE file. You may want to change this to the name of your application.

Creating an installer

Now we've successfully bundled our application, we'll next look at how we can take our dist folder and use it to create a functioning Windows installer.

To create our installer we'll be using a tool called InstallForge. InstallForge is free and you can download the installer from this page.

We'll now walk through the basic steps of creating an installer with InstallForge. If you're impatient, you can download the finished Installforge Installer here.

General

When you first run InstallForge you'll be presented with this General tab. Here you can enter the basic information about your application, including the name, program version, company and website.

InstallForge initial view, showing General settings InstallForge initial view, showing General settings

You can also select the target platforms for the installer, from various versions of Windows that are available. For desktop applications you currently probably only want to target Windows 7, 8 and 10.

Setup

Click on the left sidebar to open the "Files" page under "Setup". Here you can specify the files to be bundled in the installer.

Use "Add Files…" and select all the files in the dist/app folder produced by PyInstaller. The file browser that pops up allows multiple file selections, so you can add them all in a single go, however you need to add folders separately. Click "Add Folder…" and add any folders under dist/app such as the icons folder.

InstallForge Files view, add all files & folders to be packaged InstallForge Files view, add all files & folders to be packaged

Once you're finished scroll through the list to the bottom and ensure that the folders are listed to be included. You want all files and folders under dist/app to be present. But the folder dist/app itself should not be listed.

The default install path can be left as-is. The values between angled brackets, e.g. <company> , are variables and will be filled automatically.

Next, it's nice to allow your users to uninstall your application. Even though it's undoubtedly awesome, they may want to remove it at some time in the future. You can do this under the "Uninstall" tab, simply by ticking the box. This will also make the application appear in "Add or Remove Programs".

InstallForge add Uninstaller for your app InstallForge add Uninstaller for your app

Dialogs

The "Dialogs" section can be used to show custom messages, splash screens or license information to the user. The "Finish" tab lets you control what happens once the installer is complete, and it's helpful here to give the user the option to run your program.

To do this you need to tick the box next to "Run program" and add your own application EXE into the box. Since <installpath>\ is already specified, we can just add app.exe.

InstallForge configure optional run program on finish install InstallForge configure optional run program on finish install

System

Under "System" select "Shortcuts" to open the shortcut editor. Here you can specify shortcuts for both the Start Menu and Desktop if you like.

InstallForge configure Shortcuts, for Start Menu and Desktop InstallForge configure Shortcuts, for Start Menu and Desktop

Click "Add…" to add new shortcuts for your application. Choose between Start menu and Desktop shortcuts, and fill in the name and target file. This is the path your application EXE will end up at once installed. Since <installpath>\ is already specified, you simply need to add your application's EXE name onto the end, here app.exe

InstallForge, adding a Shortcut InstallForge, adding a Shortcut

Build

With the basic settings in place, you can now build your installer.

At this point you can save your InstallForge project so you can re-build the installer from the same settings in future.

Click on the "Build" section at the bottom to open the build panel.

InstallForge, ready to build InstallForge, ready to build

Click on the large icon button to start the build process. If you haven't already specified a setup file location you will be prompted for one. This is the location where you want the completed installer to be saved.

Don't save it in your dist folder.

The build process will began, collecting and compressing the files into the installer.

InstallForge, build complete InstallForge, build complete

Once complete you will be prompted to run the installer. This is entirely optional, but a handy way to find out if it works.

Running the installer

The installer itself shouldn't have any surprises, working as expected. Depending on the options selected in InstallForge you may have extra panels or options.

InstallForge, running the resulting installer InstallForge, running the resulting installer

Step through the installer until it is complete. You can optionally run the application from the last page of the installer, or you can find it in your start menu.

Our demo app in the Start Menu on Windows 11 Our demo app in the Start Menu in the Start Menu on Windows 11

Wrapping up

In this tutorial we've covered how to build your Tkinter applications into a distributable EXE using PyInstaller, including adding data files along with your code. Then we walked through the process of building the application into a Windows Installer using InstallForge. Following these steps you should be able to package up your own applications and make them available to other people.

For a complete view of all PyInstaller bundling options take a look at the PyInstaller usage documentation.

Which Python GUI library should you use?

Python is a popular programming used for everything from scripting routine tasks to building websites and performing complex data analysis. While you can accomplish a lot with command line tools, some tasks are better suited to graphical interfaces. You may also find yourself wanting to build a desktop front-end for an existing tool to improve usability for non-technical users. Or maybe you're building some hardware or a mobile app and want an intuitive touchscreen interface.

To create graphical user interfaces with Python, you need a GUI library. Unfortunately, at this point things get pretty confusing -- there are many different GUI libraries available for Python, all with different capabilities and licensing. Which Python GUI library should you use for your project?

In this article, we will look at a selection of the most popular Python GUI frameworks currently available and why you should consider using them for your own projects. You'll learn about the relative strengths of each library, understand the licensing limitations and see a simple Hello, World! application written in each. By the end of the article you should feel confident choosing the right library for your project.

Tkinter

Best for simple tool GUIs, small portable applications

Tkinter is the defacto GUI framework for Python. It comes bundled with Python on both Windows and macOS. (On Linux, it may require downloading an additional package from your distribution's repo.) Tkinter is a wrapper written around the Tk GUI toolkit. Its name is an amalgamation of the words Tk and Interface.

Tkinter is a simple library with support for standard layouts and widgets, as well as more complex widgets such as tabbed views & progressbars. Tkinter is a pure GUI library, not a framework. There is no built-in support for GUIs driven from data sources, databases, or for displaying or manipulating multimedia or hardware. However, if you need to make something simple that doesn't require any additional dependencies, Tkinter may be what you are looking for. Tkinter is cross-platform however the widgets can look outdated, particularly on Windows.

Installation Already installed with Python on Windows and macOS. Ubuntu/Debian Linux sudo apt install python3-tk

A simple hello world application in Tkinter is shown below.

python
import tkinter as tk

window = tk.Tk()
window.title("Hello World")


def handle_button_press(event):
    window.destroy()


button = tk.Button(text="My simple app.")
button.bind("", handle_button_press)
button.pack()

# Start the event loop.
window.mainloop()

python
from tkinter import Tk, Button


class Window(Tk):
    def __init__(self):
        super().__init__()

        self.title("Hello World")

        self.button = Button(text="My simple app.")
        self.button.bind("", self.handle_button_press)
        self.button.pack()

    def handle_button_press(self, event):
        self.destroy()


# Start the event loop.
window = Window()
window.mainloop()

Tkinter Application Screenshot Hello world application built using Tkinter, running on Windows 11

Tkinter was originally developed by Steen Lumholt and Guido Van Rossum, who designed Python itself. Both the GUI framework and the language are licensed under the same Python Software Foundation (PSF) License. While the license is compatible with the GPL, it is a 'permissive' license (similar to the MIT License) that allows it to be used for proprietary applications and modifications.

PyQt or PySide

Best for desktop applications, multimedia, scientific and engineering software

PyQt and PySide are wrappers around the Qt framework. They allow you to easily create modern interfaces that look right at home on any platform, including Windows, macOS, Linux and even Android. They also have solid tooling with the most notable being Qt Creator, which includes a WYSIWYG editor for designing GUI interfaces quickly and easily. Being backed by a commercial project means that you will find plenty of support and online learning resources to help you develop your application.

Qt (and by extension PyQt & PySide) is not just a GUI library, but a complete application development framework. In addition to standard UI elements, such as widgets and layouts, Qt provides MVC-like data-driven views (spreadsheets, tables), database interfaces & models, graph plotting, vector graphics visualization, multimedia playback, sound effects & playlists and built-in interfaces for hardware such as printing. The Qt signals and slots models allows large applications to be built from re-usable and isolated components.

While other toolkits can work great when building small & simple tools, Qt really comes into its own for building real commercial-quality applications where you will benefit from the pre-built components. This comes at the expense of a slight learning curve. However, for smaller projects Qt is not really any more complex than other libraries. Qt Widgets-based applications use platform native widgets to ensure they look and feel at home on Windows, macOS and Qt-based Linux desktops.

Installation pip install pyqt6 or pip install pyside6

A simple hello world application in PyQt6, using the Qt Widgets API is shown below.

python
from PyQt6.QtWidgets import QMainWindow, QApplication, QPushButton

import sys


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.setWindowTitle("Hello World")

        button = QPushButton("My simple app.")
        button.pressed.connect(self.close)

        self.setCentralWidget(button)
        self.show()


app = QApplication(sys.argv)
w = MainWindow()
app.exec()

python
from PySide6.QtWidgets import QMainWindow, QApplication, QPushButton

import sys


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.setWindowTitle("Hello World")

        button = QPushButton("My simple app.")
        button.pressed.connect(self.close)

        self.setCentralWidget(button)
        self.show()


app = QApplication(sys.argv)
w = MainWindow()
app.exec()

As you can see, the code is almost identical between PyQt & PySide, so it's not something to be concerned about when you start developing with either: you can always migrate easily if you need to.

PyQt6 Application Screenshot Hello world application built using PyQt6, running on Windows 11

Before the Qt Company (under Nokia) released the officially supported PySide library in 2009, Riverbank Computing had released PyQt in 1998. The main difference between these two libraries is in licensing. The free-to-use version of PyQt is licensed under GNU General Public License (GPL) v3 but PySide is licensed under GNU Lesser General Public License (LGPL). This means that PyQt is limited GPL-licensed applications unless you purchase its commercial version, while PySide may be used in non-GPL applications without any additional fee. However, note that both these libraries are separate from Qt itself which also has a free-to-use, open source version and a paid, commercial version.

For a more information see our article on PyQt vs PySide licensing.

PyQt/PySide with QML

Best for Raspberry Pi, microcontrollers, industrial and consumer electronics

When using PyQt and PySide you actually have two options for building your GUIs. We've already introduced the Qt Widgets API which is well-suited for building desktop applications. But Qt also provides a declarative API in the form of Qt Quick/QML.

Using Qt Quick/QML you have access to the entire Qt framework for building your applications. Your UI consists of two parts: the Python code which handles the business logic and the QML which defines the structure and behavior of the UI itself. You can control the UI from Python, or use embedded Javascript code to handle events and animations.

Qt Quick/QML is ideally suited for building modern touchscreen interfaces for microcontrollers or device interfaces -- for example, building interfaces for microcontrollers like the Raspberry Pi. However you can also use it on desktop to build completely customized application experiences, like those you find in media player applications like Spotify, or to desktop games.

Installation pip install pyqt6 or pip install pyside6

A simple Hello World app in PyQt6 with QML. Save the QML file in the same folder as the Python file, and run as normally.

python
import sys

from PyQt6.QtGui import QGuiApplication
from PyQt6.QtQml import QQmlApplicationEngine


app = QGuiApplication(sys.argv)

engine = QQmlApplicationEngine()
engine.quit.connect(app.quit)
engine.load('main.qml')

sys.exit(app.exec())

qml
import QtQuick 2.15
import QtQuick.Controls 2.15

ApplicationWindow {
    visible: true
    width: 600
    height: 500
    title: "HelloApp"

    Text {
        anchors.centerIn: parent
        text: "Hello World"
        font.pixelSize: 24
    }

}

Licensing for Qt Quick/QML applications is the same as for other PyQt/PySide apps.

PyQt6 QML Application Screenshot Hello world application built using PyQt6 & QML, running on Windows 11

Kivy

Best for Python mobile app development

While most other GUI frameworks are bindings to toolkits written in other programming languages, Kivy is perhaps the only framework which is primarily written in pure Python. If you want to create touchscreen-oriented interfaces with a focus on mobile platforms such as Android and iOS, this is the way to go. This does run on desktop platforms (Windows, macOS, Linux) as well but note that your application may not look and behave like a native application. However, there is a pretty large community around this framework and you can easily find resources to help you learn it online.

The look and feel of Kivy is extremely customizable, allowing it to be used as an alternative to libraries like Pygame (for making games with Python). The developers have also released a number of separate libraries for Kivy. Some provide Kivy with better integration and access to certain platform-specific features, or help package your application for distribution on platforms like Android and iOS. Kivy has it's own design language called Kv, which is similar to QML for Qt. It allows you to easily separate the interface design from your application's logic.

There is a 3rd party add-on for Kivy named KivyMD that replaces Kivy's widgets with ones that are compliant with Google's Material Design.

A simple hello world application in Kivy is shown below.

Installation pip install kivy

A simple hello world application in Kivy is shown below.

python
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.core.window import Window

Window.size = (300, 200)


class MainWindow(BoxLayout):
    def __init__(self):
        super().__init__()
        self.button = Button(text="Hello, World?")
        self.button.bind(on_press=self.handle_button_clicked)

        self.add_widget(button)

    def handle_button_clicked(self, event):
        self.button.text = "Hello, World!"


class MyApp(App):
    def build(self):
        self.title = "Hello, World!"
        return MainWindow()


app = MyApp()
app.run()

Kivy Application Screenshot Hello world application built using Kivy, running on Windows 11

An equivalent application built using the Kv declarative language is shown below.

python
import kivy
kivy.require('1.0.5')

from kivy.uix.floatlayout import FloatLayout
from kivy.app import App
from kivy.properties import ObjectProperty, StringProperty


class Controller(FloatLayout):
    '''Create a controller that receives a custom widget from the kv lang file.

    Add an action to be called from the kv lang file.
    '''
    def button_pressed(self):
        self.button_wid.text = 'Hello, World!'


class ControllerApp(App):

    def build(self):
        return Controller()


if __name__ == '__main__':
    ControllerApp().run()

python
#:kivy 1.0

:
    button_wid: custom_button

    BoxLayout:
        orientation: 'vertical'
        padding: 20

        Button:
            id: custom_button
            text: 'Hello, World?'
            on_press: root.button_pressed()

The name of the Kv file must match the name of the class from the main application -- here Controller and controller.kv.

Kivy Kv Application Screenshot Hello world application built using Kivy + Kv, running on Windows 11

In February 2011, Kivy was released as the spiritual successor to a similar framework called PyMT. While they shared similar goals and was also led by the current core developers of Kivy, where Kivy differs is in its underlying design and a professional organization which actively develops and maintains it. Kivy is licensed under the MIT license, which is a 'permissive' license that allows you to use it freely in both open source and proprietary applications. As such, you are even allowed to make proprietary modifications to the framework itself.

PySimpleGUI

Best for quickly building UIs for simple tools, very portable

PySimpleGUI aims to simplify GUI application development for Python. It doesn't reinvent the wheel but provides a wrapper around other existing frameworks such as Tkinter, Qt (PySide 2), WxPython and Remi. By doing so, it not only lowers the barrier to creating a GUI but also allows you to easily migrate from one GUI framework to another by simply changing the import statement. While there is a separate port of PySimpleGUI for each of these frameworks, the Tkinter version is considered the most feature complete with the Qt version coming in at second. At the time of writing, the other ports are still more or less a work-in-progress.

There is a fair amount of good resources to help you learn to use PySimpleGUI, including an official Cookbook and a Udemy course offered by the developers themselves. According to their project website, PySimpleGUI was initially made (and later released in 2018) because the lead developer wanted a 'simplified' GUI framework to use in his upcoming project and wasn't able to find any that met his needs.

Installation pip install pysimplegui

python
import PySimpleGUI as sg


layout = [
    [sg.Button("My simple app.")]
]

window = sg.Window("Hello World", layout)

while True:
    event, values = window.read()
    print(event, values)
    if event == sg.WIN_CLOSED or event == "My simple app.":
        break

window.close()

PySimpleGUI Application Screenshot Hello world application built using PySimpleGUI, running on Windows 11

PySimpleGUI is licensed under the same LGPL v3 license as PySide, which allows its use in proprietary applications but modifications to the framework itself must be released as open source.

WxPython

Best for simple portable desktop applications

WxPython is a wrapper for the popular, cross-platform GUI toolkit called WxWidgets. It is implemented as a set of Python extension modules that wrap the GUI components of the popular wxWidgets cross platform library, which is written in C++.

WxPython uses native widgets on most platforms, ensure that your application looks and feels at home. However, WxPython is known to have certain platform-specific quirks and it also doesn't provide the same level of abstraction between platforms as Qt for example. This may affect how easy it is to maintain cross-platform compatibility for your application.

WxPython is under active development and is also currently being reimplemented from scratch under the name 'WxPython Phoenix'. The team behind WxWidgets is also responsible for WxPython, which was initially released in 1998.

Installation pip install wxpython

python
import wx


class MainWindow(wx.Frame):
    def __init__(self, parent, title):
        wx.Frame.__init__(self, parent, title=title, size=(200, -1))

        self.button = wx.Button(self, label="My simple app.")
        self.Bind(
            wx.EVT_BUTTON, self.handle_button_click, self.button
        )

        self.sizer = wx.BoxSizer(wx.VERTICAL)
        self.sizer.Add(self.button)

        self.SetSizer(self.sizer)
        self.SetAutoLayout(True)
        self.Show()

    def handle_button_click(self, event):
        self.Close()


app = wx.App(False)
w = MainWindow(None, "Hello World")
app.MainLoop()

WxPython Application Screenshot Hello world application built using WxPython, running on Windows 11

Both WxWidgets and WxPython are licensed under a WxWindows Library License, which is a 'free software' license similar to LGPL (with a special exception). It allows both proprietary and open source applications to use and modify WxPython.

PyGObject (GTK+)

Best for developing applications for GNOME desktop

If you intend to create an application that integrates well with GNOME and other GTK-based desktop environments for Linux, PyGObject is the right choice. PyGObject itself is a language-binding to the GTK+ widget toolkit. It allows you to create modern, adaptive user interfaces that conform to GNOME's Human Interface Guidelines (HIG).

It also enables the development of 'convergent' applications that can run on both Linux desktop and mobile platforms. There are a few first-party and community-made, third-party tools available for it as well. This includes the likes of GNOME Builder and Glade, which is yet another WYSIWYG editor for building graphical interfaces quickly and easily.

Unfortunately, there aren't a whole lot of online resources to help you learn PyGObject application development, apart from this one rather well-documented tutorial. While cross-platform support does exist (e.g. Inkscape, GIMP), the resulting applications won't feel completely native on other desktops. Setting up a development environment for this, especially on Windows and macOS, also requires more steps than for most other frameworks in this article, which just need a working Python installation.

Installation Ubuntu/Debian sudo apt install python3-gi python3-gi-cairo gir1.2-gtk-4.0, macOS Homebrew brew install pygobject4 gtk+4

python
import gi
gi.require_version("Gtk", "4.0")
from gi.repository import Gtk

def on_activate(app):
    win = Gtk.ApplicationWindow(application=app)
    btn = Gtk.Button(label="Hello, World!")
    btn.connect('clicked', lambda x: win.close())
    win.set_child(btn)
    win.present()

app = Gtk.Application(application_id='org.gtk.Example')
app.connect('activate', on_activate)
app.run(None)

PyGObject Application Screenshot Hello world application built using PyGObject, running on Ubuntu Linux 21.10

PyGObject is developed and maintained under the GNOME Foundation, who is also responsible for the GNOME desktop environment. PyGObject replaces several separate Python modules, including PyGTK, GIO and python-gnome, which were previously required to create a full GNOME/GTK application. Its initial release was in 2006 and it is licensed under an older version of LGPL (v2.1). While there are some differences with the current version of LGPL (v3), the license still allows its use in proprietary applications but requires any modification to the library itself to be released as open source.

Remi

Best for web based UIs for Python applications

Remi, which stands for REMote Interface, is the ideal solution for applications that are intended to be run on servers and other headless setups. (For example, on a Raspberry Pi.) Unlike most other GUI frameworks/libraries, Remi is rendered completely in the browser using a built-in web server. Hence, it is completely platform-independent and runs equally well on all platforms.

That also makes the application's interface accessible to any computer or device with a web browser that is connected to the same network. Although access can be restricted with a username and password, it doesn't implement any security strategies by default. Note that Remi is meant to be used as a desktop GUI framework and not for serving up web pages. If more than one user connects to the application at the same time, they will see and interact with the exact same things as if a single user was using it.

Remi requires no prior knowledge of HTML or other similar web technologies. You only need to have a working understanding of Python to use it, which is then automatically translated to HTML. It also comes included with a drag n drop GUI editor that is akin to Qt Designer for PyQt and PySide.

python
import remi.gui as gui
from remi import start, App

class MyApp(App):

    def main(self):
        container = gui.VBox(width=120, height=100)

        # Create a button, with the label "Hello, World!"
        self.bt = gui.Button('Hello, World?')
        self.bt.onclick.do(self.on_button_pressed)

        # Add the button to the container, and return it.
        container.append(self.bt)
        return container

    def on_button_pressed(self, widget):
        self.bt.set_text('Hello, World!')

start(MyApp)

Remi is licensed under the Apache License v2.0, which is another 'permissive' license similar to the MIT License. The license allows using it in both open source and proprietary applications, while also allowing proprietary modifications to be made to the framework itself. Its main conditions revolve around the preservation of copyright and license notices.

Remi Application Screenshot Hello world application built using Remi, running on Chrome on Windows 11

Conclusion

If you're looking to build GUI applications with Python, there is probably a GUI framework/library listed here that fits the bill for your project. Try and weigh up the capabilities & licensing of the different libraries with the scale of your project, both now and in the future.

Don't be afraid to experiment a bit with different libraries, to see which feel the best fit. While the APIs of GUI libraries are very different, they share many underlying concepts in common and things you learn in one library will often apply in another.

You are only limited by your own imagination. So go out there and make something!

The ModelView Architecture

As you start to build more complex applications with PySide6 you'll likely come across issues keeping widgets in sync with your data. Data stored in widgets (e.g. a simple QListWidget) is not readily available to manipulate from Python — changes require you to get an item, get the data, and then set it back. The default solution to this is to keep an external data representation in Python, and then either duplicate updates to the both the data and the widget, or simply rewrite the whole widget from the data. This can get ugly quickly, and results in a lot of boilerplate just for fiddling the data.

Thankfully Qt has a solution for this — ModelViews. ModelViews are a powerful alternative to the standard display widgets, which use a regular model interface to interact with data sources — from simple data structures to external databases. This isolates your data, allowing it to be kept in any structure you like, while the view takes care of presentation and updates.

This tutorial introduces the key aspects of Qt's ModelView architecture and uses it to build simple desktop Todo application in PySide.

Model View Controller

Model–View–Controller (MVC) is an architectural pattern used for developing user interfaces which divides an application into three interconnected parts. This separates the internal representation of data from how information is presented to and accepted from the user.

The MVC design pattern decouples three major components —

  • Model holds the data structure which the app is working with.
  • View is any representation of information as shown to the user, whether graphical or tables. Multiple views of the same data model are allowed.
  • Controller accepts input from the user, transforming it into commands to for the model or view.

It Qt land the distinction between the View & Controller gets a little murky. Qt accepts input events from the user (via the OS) and delegates these to the widgets (Controller) to handle. However, widgets also handle presentation of the current state to the user, putting them squarely in the View. Rather than agonize over where to draw the line, in Qt-speak the View and Controller are instead merged together creating a Model/ViewController architecture — called "Model View" for simplicity sake.

Importantly, the distinction between the data and how it is presented is preserved.

The Model View

The Model acts as the interface between the data store and the ViewController. The Model holds the data (or a reference to it) and presents this data through a standardised API which Views then consume and present to the user. Multiple Views can share the same data, presenting it in completely different ways.

You can use any "data store" for your model, including for example a standard Python list or dictionary, or a database (via e.g. SQLAlchemy) — it's entirely up to you.

The two parts are essentially responsible for —

  1. The model stores the data, or a reference to it and returns individual or ranges of records, and associated metadata or display instructions.
  2. The view requests data from the model and displays what is returned on the widget.

There is an in-depth discussion of the Qt architecture in the documentation.

A simple Model View — a Todo List

To demonstrate how to use the ModelViews in practise, we'll put together a very simple implementation of a desktop Todo List. This will consist of a QListView for the list of items, a QLineEdit to enter new items, and a set of buttons to add, delete, or mark items as done.

The UI

The simple UI was laid out using Qt Creator and saved as mainwindow.ui. The .ui file and all the other parts can be downloaded below.

Todo application Source Code

Designing a Simple Todo app in Qt Creator Designing a Simple Todo app in Qt Creator

The running app is shown below.

The running Todo GUI (nothing works yet) The running Todo GUI (nothing works yet)

The widgets available in the interface were given the IDs shown in the table below.

objectName Type Description
todoView QListView The list of current todos
todoEdit QLineEdit The text input for creating a new todo item
addButton QPushButton Create the new todo, adding it to the todos list
deleteButton QPushButton Delete the current selected todo, removing it from the todos list
completeButton QPushButton Mark the current selected todo as done

We'll use these identifiers to hook up the application logic later.

The Model

We define our custom model by subclassing from a base implementation, allowing us to focus on the parts unique to our model. Qt provides a number of different model bases, including lists, trees and tables (ideal for spreadsheets).

For this example we are displaying the result to a QListView. The matching base model for this is QAbstractListModel. The outline definition for our model is shown below.

python
class TodoModel(QtCore.QAbstractListModel):
    def __init__(self, *args, todos=None, **kwargs):
        super(TodoModel, self).__init__(*args, **kwargs)
        self.todos = todos or []

    def data(self, index, role):
        if role == Qt.DisplayRole:
            # See below for the data structure.
            status, text = self.todos[index.row()]
            # Return the todo text only.
            return text

    def rowCount(self, index):
        return len(self.todos)

The.todos variable is our data store and the two methods rowcount() and data() are standard Model methods we must implement for a list model. We'll go through these in turn below.

.todos list

The data store for our model is .todos, a simple Python list in which we'll store a tuple of values in the format [(bool, str), (bool, str), (bool, str)] where bool is the done state of a given entry, and str is the text of the todo.

We initialise self.todo to an empty list on startup, unless a list is passed in via the todos keyword argument.

self.todos = todos or [] will set self.todos to the provided todos value if it is truthy (i.e. anything other than an empty list, the boolean False or None the default value), otherwise it will be set to the empty list [].

To create an instance of this model we can simply do —

python
model = TodoModel()   # create an empty todo list

Or to pass in an existing list —

python
todos = [(False, 'an item'), (False, 'another item')]
model = TodoModel(todos)

.rowcount()

The .rowcount() method is called by the view to get the number of rows in the current data. This is required for the view to know the maximum index it can request from the data store (row count-1). Since we're using a Python list as our data store, the return value for this is simply the len() of the list.

.data()

This is the core of your model, which handles requests for data from the view and returns the appropriate result. It receives two parameters index and role.

index is the position/coordinates of the data which the view is requesting, accessible by two methods .row() and .column() which give the position in each dimension.

For our QListView the column is always 0 and can be ignored, but you would need to use this for 2D data in a spreadsheet view.

role is a flag indicating the type of data the view is requesting. This is because the .data() method actually has more responsibility than just the core data. It also handles requests for style information, tooltips, status bars, etc. — basically anything that could be informed by the data itself.

The naming of Qt.DisplayRole is a bit weird, but this indicates that the view is asking us "please give me data for display". There are other roles which the data can receive for styling requests or requesting data in "edit-ready" format.

Role Value Description
Qt.DisplayRole 0 The key data to be rendered in the form of text. (QString)
Qt.DecorationRole 1 The data to be rendered as a decoration in the form of an icon. (QColor, QIcon or QPixmap)
Qt.EditRole 2 The data in a form suitable for editing in an editor. (QString)
Qt.ToolTipRole 3 The data displayed in the item's tooltip. (QString)
Qt.StatusTipRole 4 The data displayed in the status bar. (QString)
Qt.WhatsThisRole 5 The data displayed for the item in "What's This?" mode. (QString)
Qt.SizeHintRole 13 The size hint for the item that will be supplied to views. (QSize)

For a full list of available roles that you can receive see the Qt ItemDataRole documentation. Our todo list will only be using Qt.DisplayRole and Qt.DecorationRole.

Basic implementation

Below is the basic stub application needed to load the UI and display it. We'll add our model code and application logic to this base.

python
import sys

from PySide6 import QtCore, QtGui, QtWidgets
from PySide6.QtCore import Qt

from MainWindow import Ui_MainWindow


class TodoModel(QtCore.QAbstractListModel):
    def __init__(self, todos=None):
        super().__init__()
        self.todos = todos or []

    def data(self, index, role):
        if role == Qt.DisplayRole:
            status, text = self.todos[index.row()]
            return text

    def rowCount(self, index):
        return len(self.todos)


class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
    def __init__(self):
        super().__init__()
        self.setupUi(self)
        self.model = TodoModel()
        self.todoView.setModel(self.model)


app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()


We define our TodoModel as before, and initialise the MainWindow object. In the __init__ for the MainWindow we create an instance of our todo model and set this model on the todo_view. Save this file as todo.py and run it with —

bash
python3 todo.py

While there isn't much to see yet, the QListView and our model are actually working — if you add some default data you'll see it appear in the list.

python
self.model = TodoModel(todos=[(False, 'my first todo')])

QListView showing hard-coded todo item QListView showing hard-coded todo item

You can keep adding items manually like this and they will show up in order in the QListView. Next we'll make it possible to add items from within the application.

First create a new method on the MainWindow named add. This is our callback which will take care of adding the current text from the input as a new todo. Connect this method to the addButton.pressed signal at the end of the __init__ block.

python
class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
    def __init__(self):
        QtWidgets.QMainWindow.__init__(self)
        Ui_MainWindow.__init__(self)
        self.setupUi(self)
        self.model = TodoModel()
        self.todoView.setModel(self.model)
        # Connect the button.
            self.addButton.pressed.connect(self.add)

    def add(self):
        """
        Add an item to our todo list, getting the text from the QLineEdit .todoEdit
        and then clearing it.
        """
        text = self.todoEdit.text()
        if text: # Don't add empty strings.
            # Access the list via the model.
            self.model.todos.append((False, text))
            # Trigger refresh.
            self.model.layoutChanged.emit()
            # Empty the input
            self.todoEdit.setText("")


In the add block notice the line self.model.layoutChanged.emit(). Here we're emitting a model signal .layoutChanged to let the view know that the shape of the data has been altered. This triggers a refresh of the entirety of the view. If you omit this line, the todo will still be added but the QListView won't update.

If just the data is altered, but the number of rows/columns are unaffected you can use the .dataChanged() signal instead. This also defines an altered region in the data using a top-left and bottom-right location to avoid redrawing the entire view.

Hooking up the other actions

We can now connect the rest of the button's signals and add helper functions for performing the delete and complete operations. We add the button signals to the __init__ block as before.

python
        self.addButton.pressed.connect(self.add)
        self.deleteButton.pressed.connect(self.delete)
        self.completeButton.pressed.connect(self.complete)

Then define a new delete method as follows —

python
    def delete(self):
        indexes = self.todoView.selectedIndexes()
        if indexes:
            # Indexes is a list of a single item in single-select mode.
            index = indexes[0]
            # Remove the item and refresh.
            del self.model.todos[index.row()]
            self.model.layoutChanged.emit()
            # Clear the selection (as it is no longer valid).
            self.todoView.clearSelection()


We use self.todoView.selectedIndexes to get the indexes (actually a list of a single item, as we're in single-selection mode) and then use the .row() as an index into our list of todos on our model. We delete the indexed item using Python's del operator, and then trigger a layoutChanged signal because the shape of the data has been modified.

Finally, we clear the active selection since the item it relates to may now out of bounds (if you had selected the last item).

You could try make this smarter, and select the last item in the list instead

The complete method looks like this —

python

    def complete(self):
        indexes = self.todoView.selectedIndexes()
        if indexes:
            index = indexes[0]
            row = index.row()
            status, text = self.model.todos[row]
            self.model.todos[row] = (True, text)
            # .dataChanged takes top-left and bottom right, which are equal
            # for a single selection.
            self.model.dataChanged.emit(index, index)
            # Clear the selection (as it is no longer valid).
            self.todoView.clearSelection()

This uses the same indexing as for delete, but this time we fetch the item from the model .todos list and then replace the status with True.

We have to do this fetch-and-replace, as our data is stored as Python tuples which cannot be modified.

The key difference here vs. standard Qt widgets is that we make changes directly to our data, and simply need to notify Qt that some change has occurred — updating the widget state is handled automatically.

Using Qt.DecorationRole

If you run the application now you should find that adding and deleting both work, but while completing items is working, there is no indication of it in the view. We need to update our model to provide the view with an indicator to display when an item is complete. The updated model is shown below.

python
tick = QtGui.QImage('tick.png')


class TodoModel(QtCore.QAbstractListModel):
    def __init__(self, *args, todos=None, **kwargs):
        super(TodoModel, self).__init__(*args, **kwargs)
        self.todos = todos or []

    def data(self, index, role):
        if role == Qt.DisplayRole:
            _, text = self.todos[index.row()]
            return text

        if role == Qt.DecorationRole:
            status, _ = self.todos[index.row()]
            if status:
                return tick

    def rowCount(self, index):
        return len(self.todos)


We're using a tick icon tick.png to indicate completed items, which we load into a QImage object named tick. In the model we've implemented a handler for the Qt.DecorationRole which returns the tick icon for rows who's status is True (for complete).

The icon I'm using is taken from the Fugue set by p.yusukekamiyamane

Instead of an icon you can also return a color, e.g. QtGui.QColor('green') which will be drawn as solid square.

Running the app you should now be able to mark items as complete.

Todos Marked Complete Todos Marked Complete

A persistent data store

Our todo app works nicely, but it has one fatal flaw — it forgets your todos as soon as you close the application While thinking you have nothing to do when you do may help to contribute to short-term feelings of Zen, long term it's probably a bad idea.

The solution is to implement some sort of persistent data store. The simplest approach is a simple file store, where we load items from a JSON or Pickle file at startup, and write back on changes.

To do this we define two new methods on our MainWindow class — load and save. These load data from a JSON file name data.json (if it exists, ignoring the error if it doesn't) to self.model.todos and write the current self.model.todos out to the same file, respectively.

python
    def load(self):
        try:
            with open('data.json', 'r') as f:
                self.model.todos = json.load(f)
        except Exception:
            pass

    def save(self):
        with open('data.json', 'w') as f:
            data = json.dump(self.model.todos, f)

To persist the changes to the data we need to add the .save() handler to the end of any method that modifies the data, and the .load() handler to the __init__ block after the model has been created.

The final code looks like this —

python
import json
import sys

from PySide6 import QtCore, QtGui, QtWidgets
from PySide6.QtCore import Qt

from MainWindow import Ui_MainWindow

tick = QtGui.QImage("tick.png")


class TodoModel(QtCore.QAbstractListModel):
    def __init__(self, todos=None):
        super().__init__()
        self.todos = todos or []

    def data(self, index, role):
        if role == Qt.DisplayRole:
            _, text = self.todos[index.row()]
            return text

        if role == Qt.DecorationRole:
            status, _ = self.todos[index.row()]
            if status:
                return tick

    def rowCount(self, index):
        return len(self.todos)


class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
    def __init__(self):
        super().__init__()

        self.setupUi(self)
        self.model = TodoModel()
        self.load()
        self.todoView.setModel(self.model)
        self.addButton.pressed.connect(self.add)
        self.deleteButton.pressed.connect(self.delete)
        self.completeButton.pressed.connect(self.complete)

    def add(self):
        """
        Add an item to our todo list, getting the text from the QLineEdit .todoEdit
        and then clearing it.
        """
        text = self.todoEdit.text()
        if text:  # Don't add empty strings.
            # Access the list via the model.
            self.model.todos.append((False, text))
            # Trigger refresh.
            self.model.layoutChanged.emit()
            # Empty the input
            self.todoEdit.setText("")
            self.save()

    def delete(self):
        indexes = self.todoView.selectedIndexes()
        if indexes:
            # Indexes is a list of a single item in single-select mode.
            index = indexes[0]
            # Remove the item and refresh.
            del self.model.todos[index.row()]
            self.model.layoutChanged.emit()
            # Clear the selection (as it is no longer valid).
            self.todoView.clearSelection()
            self.save()

    def complete(self):
        indexes = self.todoView.selectedIndexes()
        if indexes:
            index = indexes[0]
            row = index.row()
            status, text = self.model.todos[row]
            self.model.todos[row] = (True, text)
            # .dataChanged takes top-left and bottom right, which are equal
            # for a single selection.
            self.model.dataChanged.emit(index, index)
            # Clear the selection (as it is no longer valid).
            self.todoView.clearSelection()
            self.save()

    def load(self):
        try:
            with open("data.json", "r") as f:
                self.model.todos = json.load(f)
        except Exception:
            pass

    def save(self):
        with open("data.json", "w") as f:
            data = json.dump(self.model.todos, f)


app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()



If the data in your application has the potential to get large or more complex, you may prefer to use an actual database to store it. In this case the model will wrap the interface to the database and query it directly for data to display. I'll cover how to do this in an upcoming tutorial.

For another interesting example of a QListView see this example media player application. It uses the Qt built-in QMediaPlaylist as the datastore, with the contents displayed to a QListView.