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.

CXX-Qt 0.4 Released

We just released CXX-Qt version 0.4!

CXX-Qt is a set of Rust crates for creating bidirectional Rust ⇄ C++ bindings with Qt. It can be used to integrate Rust into C++ applications using CMake or build Rust applications with Cargo. CXX-Qt provides tools for implementing QObject subclasses in Rust that can be used from C++, QML, and JavaScript.

For 0.4, most of the internals of CXX-Qt have been refactored into stages, which means it is easier to maintain. There are various small changes to the API, such as the capability of using attributes in more places rather than magic struct names. Relocatable Qt types have been marked as trivial to CXX (which means they don’t need to be wrapped in pointers).

Some of the larger developer-facing changes are listed below.

Refactor of API So That CXX-Qt is a Superset of CXX

You can now combine CXX definitions into the CXX-Qt bridge macro.

This allows you to define custom types with CXX extern blocks that can then be used for CXX-Qt purposes, such as properties, invokables, and signals.

For example, in the bridge below, a CXX extern “C++” block is used to define QString as a type. Then, this can be used as a property in the QObject definition.

#[cxx_qt::bridge]
mod my_object {
    unsafe extern "C++" {
        include!("cxx-qt-lib/qstring.h");
        type QString = cxx_qt_lib::QString;
    }

    #[cxx_qt::qobject]
    #[derive(Default)]
    pub struct MyObject {
        #[qproperty]
        number: i32,
        #[qproperty]
        string: QString,
    }
}

Rewrite of the Build System

Custom CMake files have been removed from the build process; Corrosion is used instead, for CMake builds. This allows for a cleaner build that configures and builds Rust code at the correct times.

There is also support for Cargo-only builds for Rust developers who don’t use CMake.

Simplified Thread Queueing

Rust closures can now be used to queue tasks onto the Qt event loop from a Rust background thread. This allows you to capture items rather than use a channel with function pointers, as we did in the previous release.

Thanks to Yuya Nishihara for this contribution.

// In an invokable request a handle to the qt thread
let qt_thread = self.qt_thread();
// Spawn a Rust thread
std::thread::spawn(move || {
    let value = compute_value_on_rust_thread();
    // Use a closure to move the value and run the task on the Qt event loop
    qt_thread
        .queue(move |mut qobject| {
            // Happens on the Qt event loop
            qobject.set_value(value);
        })
        .unwrap();
});

For More Information

Find CXX-Qt on our GitHub page.

We also have a Rust book with a getting started guide.

Discussions and contributions are welcome, as the API continues evolving for 0.5. With 0.5, we hope to improve the ability to implement listmodels from Rust and introduce Qt container types.

 

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 CXX-Qt 0.4 Released appeared first on KDAB.

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!

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.

C/C++ Debugging Tools

In this blog on debugging and profiling, I would like to give you an overview of the debugging tools that exist for C and C++ applications.

Automated Testing

The first thing you need to do is make sure your code is properly tested. This, in itself, is not debugging, but it enables you to make sure that you don’t introduce three new bugs when you fix one. One of the ideas that you can use for this is called Test-Driven-Development, which means writing the tests before you write the code that it will test. This way, you can make sure the test is testing the right thing. If the test fails, you fix it, and then it passes. It’s a very good idea to add full unit test coverage for classes you’re going to rewrite or refactor, so that you don’t introduce regressions compared to the old code. To do this, you can use one of the existing unit test frameworks. As a Qt developer, I know especially QTestLib. But you can also use Google Test or Catch and there are actually many others. The goals of those is to save you time because you don’t have to set up everything so you can write a test, make sure that all the test methods are called, how to handle failures, and all of that.

One step further is to integrate these tests with your continuous integration so that, after every commit or every night, you get a full build from scratch and a full run of all of the unit tests. All of this improves the quality of your application and is very necessary for the actual debugging that we are going to talk about because you’ll make sure you don’t introduce regressions when actually fixing a bug.

Code Coverage Tools

Another thing that is quite related to unit testing is code coverage. You want to make sure that you have a sufficient number of unit tests, and that they cover an important percentage of your code. Note however that it’s usually pointless to try to go to 100% coverage. What’s important is to test the parts of the application that are critical to you. That could be simple utility classes that are used everywhere, like the string class or the vector class if you have one of those. Or it can be the part of the application where the actual business logic is and you don’t want any bugs in it. One thing you can do to find out if your unit tests cover enough of the important code is set up code coverage.

There are tools that will tell you 90% of these files have been covered by unit tests and you can then figure out exactly which part of the code you’re missing in your tests. There are many tools for doing that. The most well known of them is gcov on Linux. It actually goes together with the compilers GCC and Clang, since those generate the information for gcov, when passing the --coverage compiler flag. There are also tools on Windows as part of Visual Studio, or you can install OpenCppCoverage. Also, Squish Coco is another one. All of these tools allow you to measure how much of your code is actually tested by your unit tests. This is an area where there are many, many concurrent solutions because it’s something that’s very much used in some domains, for instance anything that will fly in a plane has to be very well unit tested and covered by those tests. These are domains where code coverage is really important.

Static Code Analysis Tools

Another thing you can do to improve the quality of your code is set up static code analysis. This is another area with many different tools. The goal is to have bug detection without even running the application, simply having a tool that looks at the code and tells you where there’s a construct or pattern that might have a bug. It’s then up to a developer to look into it. These days, the most well-known tools for this are actually the compilers themselves. If you enable sufficient amounts of warnings in GCC and Clang, or Visual Studio, you will get feedback from your compiler about what you can improve in your code. So my recommendation would be to enable as many of those warnings as you can in order to detect as many problems as possible. Also, use more compilers than just one. If you can use two or three, that’s better because you will get different feedback from the compilers. Then, there are some additional tools that you can use. A well-known one is clang-tidy, which comes with many checks for C++ code, some of which even come with automatic fixing of the code. Using the same clang-based libraries, a KDAB developer, Sérgio Martins, wrote a plugin for Clang called Clazy, which allows the detection of common coding errors, especially when using Qt but also in general C++ applications. Then, there are many more specialized tools, like Coverity, PVS-Studio, and so many others. It’s quite interesting to look at, for instance, what PVS-Studio can find by reading their blogs. They have lots of interesting finds in open source applications, for instance. It’s quite educational. This is also something that you can set up as part of your continuous integration so you get regular feedback on what can be improved in your code.

Logging (debug messages)

Let’s talk about things that look a little bit more like what you would expect in terms of debugging. The most well-known debugging technique would be printf or equivalent: some logging of messages for figuring out into which function we’re going into, what is the value of this variable, and so on. So, there are many variants to this: obviously, the ones that are part of C and C++, like printf and cout, and all those that come with your framework, like qDebug in Qt, or additional libraries, like log4cxx, boost::log, log4cplus, easylogging++… One thing most of those have in common is that they make install to turn on and off whole sets of debugging statements. So, if you are debugging a parser you would only enable the parser set of messages or if you are debugging printing you would enable all of the messages that are about printing, and so on. This is a lot more practical than having 10,000 messages per second and then you have to figure this out, or having everything off by default and you need to comment what you need. This is a lot easier if you can just toggle a switch somewhere and get what you need.

Assertions

Now let’s talk about assertions. These are, at runtime, a way to say “this will never happen”. If it does happen, stop the application and let me debug why it happened. That is something you would use for logic errors. You’d think your program is done in a way that this can never happen. This is especially useful for preconditions, like this pointer shouldn’t be known before passed to this function; or postconditions, like this function will never return a unit pointer; or invariants, things that shouldn’t change while the function is being executed. But, of course, it’s useful for many other things as well. You shouldn’t use it for runtime errors, like this file was not found. That is not really a logic error, it’s more of a setup problem. You don’t want to abort your application just because the user misplaced a file.

Another kind of assertion is the static assertion. Those are part of the compilation step. The compiler will tell you that a check failed, so you have to look into it. For instance, you would use this for an enum with the wrong amount of values, for inconsistent read only data, for inheritance that isn’t as we expected, for unsupported CPU architecture, and so on.

Tracing

Let’s talk about tracing. One of the things you can do is investigate which dynamic libraries are being used by your application. That’s something you would do by using tools like ldd on Linux, Dependencies.exe on Windows, or otool on MacOS. This will simply list the shared libraries that are used by your application because possibly you are not using the one you thought and this could be the reason for the bug.

At runtime you can also figure out what your application is doing, as a black box. From the outside, you ask the application to show you all the files it’s opening. That’s something you can do with strace or the equivalent tools on other operating systems. You can ask it to show you any time it’s going to (attempt to) open a file, use a socket or anything else that goes through a system call. It’s a common debugging technique when you don’t actually have the source code for your application, but it’s also very useful if you do.

Debuggers

Then we have the very well-known debuggers, like gdb on Linux, lldb on MacOS, cdb on Windows, which allows you to do step-by-step debugging in your application. What you might not know is that there’s another debugger called RR, which allows you to go backwards and forwards in your application. Isn’t that amazing? The way it works is you record the run of your application, hopefully triggering the bug you’re after. Then, when you replay that recording (in gdb, launched by RR), you can go backwards and forwards and skip ahead, skip backwards, and so on. You can navigate through the recorded run of your application any way you like. That is extremely useful when you would usually hit that problem with gdb where you think the value is wrong and wonder how it’s calculated and need to go back in time to find out. You can’t do that because you don’t have a time machine (at least I don’t). But RR has the full recording and can go back because it has recorded all of the information to be able to do that. So, that’s a very worthy tool to look into.

KDAB’s GammaRay

For those of you doing Qt development, I want to tell you about a tool that was developed by KDAB, called GammaRay. It’s free and open source, you can get it on github. What it can do is introspect a Qt application, which means look into it and tell you all of the QObjects that are present, show you all the QWidgets graphically, same for all the QML elements in the scene, and all of the 3D elements in the Qt 3D scene, and so on, and so on. It has a large number of modules where you can get all of that information and know more about your application. It’s not a debugger, per se. It’s not going to go step-by-step in your code. This is much more like getting an overview of your Qt application and everything that it has created, and it’s a way to debug problems like why a widget isn’t big enough. Is the minimum size wrong? Is the size policy wrong? You can get information about all of these properties from GammaRay. It even supports remote debugging if you’re doing embedded development and it supports attaching to a running application, which is also quite interesting.

Valgrind

Now let me tell you about Valgrind. This is a tool that is especially known on Linux. It also works on MacOS. It will run your application in a very slow way. It takes its time, but it will look at what your application is doing, once compiled right. It’s running on the binary. It will tell you things like you are using memory after you deleted it — that’s very bad — or you are using this bit of memory without initializing it, and all of these kinds of errors. So that’s quite useful when you have behavior that doesn’t seem to be reproducible. You run the application again and get a different result. That’s the type of bug where Valgrind really shines because it will tell you exactly where this source of invalid memory usage comes from. It has other tools besides this one. It has helgrind for race conditions. It has massif for memory usage and it has callgrind for profiling. For these three things, there are alternatives listed in this blog that end up actually being better. But the default tool, memcheck, is very, very useful.

Sanitizers

Finally, let’s talk about sanitizers. This is another way to detect problems like the use of invalid memory or debug deletion. Instead of doing that with an outside tool, what we can do with sanitizers is actually ask the compiler to inject code into your own code that does all of these verifications. Any time your code is going to allocate memory, it will remember that. Any time your code will use an area of memory, it will first check that it’s able to do so. This is extremely powerful. It’s a lot faster than Valgrind. It is part of your compilers, if you’re using GCC 4.9 (or later) or Clang 3.1 (or later) on Linux or if you’re using Clang 6 (or later) or Visual Studio 2019 16.4 (or later) on Windows.

There are four types of sanitizers:

  • the address sanitizer will tell you about all of the memory usage issues
  • the leak sanitizer (included in the address sanitizer, but available separately) will tell you about memory leaks
  • the thread sanitizer is extremely good at telling you about race conditions in particular, which is something that’s really hard to detect otherwise
  • the undefined behavior sanitizer tells you when your code has some undefined behavior

There are some additional sanitizers being developed, such as the memory sanitizer to detect uninitialized memory, but this one requires recompiling all your libraries with it, so it’s not really convenient.

This was a summary of all of the debugging tools that are available for C and C++ applications. If you’d like more details about any of these, we actually do full trainings on this so that you can learn more about using these tools, what they can do in practice, and then do some exercises to get used to them.

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 C/C++ Debugging Tools 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!

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.

The Future of CI

For years, we at KDAB have been using Buildbot as our build and continuous integration system. Gerrit hosts all our projects and is our code review platform. Our deployment of Buildbot and build machines has naturally grown over the years. It builds hundreds of configurations and up to a thousand builds daily, but issues with reliability and quality of service called for a major restructuring. Over the past year, we gradually developed and migrated to new infrastructure and, once that was in place, we were finally able to add some long-awaited features.

Buildbot at KDAB

Buildbot is a continuous integration system written in Python. It offers a high degree of flexibility, because the build configuration is fully written in Python. Thanks to an extensive homegrown library of functions and features, we need only a few lines of code to build, test and package a new C++ and Qt/QML application on Linux, Windows, Mac as well as Android, iOS and other platforms. Many of our projects build against multiple different compilers and Qt versions per platform. Sometimes application bundles need to be signed and notarized (looking at you, Apple). Once the build is finished, the apps are offered for download from our servers and developer app stores like Visual Studio App Center. Developers are notified about build failures per email, our chat system and, of course, in our code review tool Gerrit.

System Architecture

From a system architecture point of view, Buildbot follows a master/worker paradigm: Workers are virtual machines, bare metal servers or docker containers with build environments. Each worker runs a Buildbot process which receives and executes build commands. A central master keeps track of changes in Gerrit, build configurations, workers, builds and build results. In large deployments like ours, the master is made up of multiple Buildbot processes with different responsibilities: One process to serve the web interface, and another to coordinate builds. They rely on a variety of additional services: MariaDB for data storage, Apache2 to manage user authentication and Crossbar.io to coordinate messages between the master’s Buildbot processes. The Buildbot master centrally controls how builds are run on the workers and issues every build command individually to the respective worker.

Diagram of Buildbot Infrastructure at KDAB

This setup enabled, among others, the following features:

  • build 100s of projects, configurations and builds daily, 
  • monitor 100s of Git repositories for changes, hosted on our internal Gerrit, on GitHub and other platforms
  • report build results via email, into our chat system, to Gerrit and GitHub – accessible to customers and KDABians alike
  • web interface to provide insight into builds (for KDABians only) and build artifacts

Over the past years, the number of projects and daily builds has grown steadily. The speed of builds and of the web interface degraded correspondingly. The master processes and all accompanying services were hosted on a single VM with a networked file system – and the file system was quickly identified as main bottleneck. Unfortunately, the traditional approach to deployment hampered our efforts to move the system to bare metal hardware with decent file system speed: All dependencies, the services and Buildbot itself were installed directly on the system using the system package manager and Python’s pip. Buildbot configuration relied heavily on hard-coded values, including IP addresses and file system paths. For every new instance we would have to recreate the setup step by step, a very laborious process that would have left us with a setup just as inflexible as the old one.

Modern Buildbot at KDAB

Therefore, we decided to encapsulate Buildbot and all services in Docker containers and use Docker Compose to describe the whole stack. To that end, we undertook the following steps:

  • refactor the Buildbot configuration so that the configuration supports multiple independent instances of the Buildbot master:
    • read instance-specific parameters like URL, website name, address of back-end services, etc. from environment variables
    • allow to read different Python scripts to describe builds and build workers, configured via environment variables
  • create Docker images
    • for Buildbot, including all dependencies and our custom patches
    • for Apache2
    • for an SSH server to receive artifact uploads from workers
    • a monitoring solution (Telegraf) to collect performance data from the services
    • and more
  • create a Docker Compose configuration to describe the services and their interactions

The Docker Compose configuration is instance-independent: In order to create a new instance of Buildbot master, with its own set of workers, build configuration, URL, name and so forth, we simply copy the Docker Compose configuration to the host system and create an environment file which describes the instance. Now, setting up a new Buildbot instance.

Quality of Service Improved

Provided with these new tools, we directly created two new Buildbot instances: One to replace the old instance for KDAB-internal and customer projects, and another to build projects of one of our larger customers. For this customer we already provided code hosting and continuous integration services, but their developers could not directly access Buildbot’s web interface with details on the builds, due to limitations in Buildbot’s access management. The new separate instance removes this obstacle, and our customer now has full access to builds.

The transition brought about many other improvements to our customers, developers and system administrators:

  • Build speed improved drastically: The time from creation of a commit on Gerrit to the beginning of the respective builds lowered from typically more than 5 minutes to less than a second. The overall build time often sank by more than 50 %.
  • The system became more reliable with less crashes and freezes.
  • Time to update build configuration on the fly, e.g. to add a new project, decreased from minutes to a few seconds.
  • Special configuration of Apache2 allows to easily brand the Buildbot web interface not only by changing the instance’s name but also by changing colors, a feature which Buildbot does not offer natively.
  • We can offer dedicated Buildbot instances to customers so that they have direct access to build results.

Build Results in Gerrit

For years platforms like GitHub present build results in the web interface and have build failures block merges. Gerrit has only offered rudimentary support for this. Buildbot could block submission of patches. It could also create comments to inform about build errors. But the presentation in Gerrit was cluttered and often confusing, a workaround.

Luckily, Gerrit 3.4 introduced the Checks API. JavaScript plug-ins in Gerrit fetch information from a CI system and supply it to the Gerrit web interface. Gerrit then displays the build results right there with the commit message and review comments. The interface shows every build configuration separately, and even build output like compile errors or test failures are right there in Gerrit. The Gerrit project provides an example of what it can look like:

Example of Gerrit's Check UI

So far, there is no plug-in for Buildbot publicly available, so we developed our own. Now, when a developer opens a change on Gerrit, our JavaScript plug-in queries Buildbot’s REST API for builds. The script will automatically determine the correct instance of Buildbot, and whether the user has access to that particular instance at all.

We needed to come up with a few tricks to make this happen. First, Buildbot did not allow to efficiently query builds for a Gerrit change. We added that feature, provided the patch to Buildbot upstream, and deployed it on our instances. This, by the way, is not our first contribution to Buildbot. Over the years we contributed 40 patches. Second, we introduced an additional endpoint on the web server to gracefully check whether a Gerrit user has access to a particular Buildbot instance in the first place. We do this to avoid unnecessary log-in dialogs in the web interface. Third, we created a custom data store in Gerrit to map repositories to Buildbot instances.

The new Docker Compose configuration helped significantly with this: We could easily develop and test all of these changes on local development instances of Buildbot. Deployment to the production instances was also quick and efficient. Fundamentally, without the performance improvements that the new instances brought, this feature would have not been possible. Feedback by our developers has been great so far.

Conclusion

This is not all, of course. We are currently looking into using Docker and VM images to create reproducible build environments. Developers get access and can then debug build failures  in the exact same environment as the CI. We are also investigating ways to upstream the Gerrit plug-in.

We at KDAB consider best practices and efficient workflows to be a large part of creating great software. That is why we keep investing into better infrastructure, for ourselves and for our customers. If you’d like to learn details about our infrastructure or discuss similar projects, feel free to get in touch.

 

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 The Future of CI appeared first on KDAB.

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.

Packaging PyQt6 applications into a macOS app with PyInstaller

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 PyQt6. 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 more complex PyQt6 applications into distributable macOS app bundles. 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 a macOS Disk Image, the usual method for distributing applications on macOS.

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 Disk Image for macOS Example Disk Image Installer for macOS

If you're impatient, you can download the Example Disk Image for macOS first.

Requirements

PyInstaller works out of the box with PyQt6 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 PyQt6 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 PyQt6 you would use —

python
pip3 install PyQt6 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 PyQt6 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 macOS).

Simple skeleton app in PyQt6 Simple skeleton app in PyQt6

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 --windowed app.py

The --windowed flag is necessary to tell PyInstaller to build a macOS .app bundle.

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
martin@MacBook-Pro pyqt6 % pyinstaller --windowed app.py
74 INFO: PyInstaller: 4.8
74 INFO: Python: 3.9.9
83 INFO: Platform: macOS-10.15.7-x86_64-i386-64bit
84 INFO: wrote /Users/martin/app/pyqt6/app.spec
87 INFO: UPX is not available.
88 INFO: Extending PYTHONPATH with paths
['/Users/martin/app/pyqt6']
447 INFO: checking Analysis
451 INFO: Building because inputs changed
452 INFO: Initializing module dependency graph...
455 INFO: Caching module graph hooks...
463 INFO: Analyzing base_library.zip ...
3914 INFO: Processing pre-find module path hook distutils from '/usr/local/lib/python3.9/site-packages/PyInstaller/hooks/pre_find_module_path/hook-distutils.py'.
3917 INFO: distutils: retargeting to non-venv dir '/usr/local/Cellar/python@3.9/3.9.9/Frameworks/Python.framework/Versions/3.9/lib/python3.9'
6928 INFO: Caching module dependency graph...
7083 INFO: running Analysis Analysis-00.toc
7091 INFO: Analyzing /Users/martin/app/pyqt6/app.py
7138 INFO: Processing module hooks...
7139 INFO: Loading module hook 'hook-PyQt6.QtWidgets.py' from '/usr/local/lib/python3.9/site-packages/PyInstaller/hooks'...
7336 INFO: Loading module hook 'hook-xml.etree.cElementTree.py' from '/usr/local/lib/python3.9/site-packages/PyInstaller/hooks'...
7337 INFO: Loading module hook 'hook-lib2to3.py' from '/usr/local/lib/python3.9/site-packages/PyInstaller/hooks'...
7360 INFO: Loading module hook 'hook-PyQt6.QtGui.py' from '/usr/local/lib/python3.9/site-packages/PyInstaller/hooks'...
7397 INFO: Loading module hook 'hook-PyQt6.QtCore.py' from '/usr/local/lib/python3.9/site-packages/PyInstaller/hooks'...
7422 INFO: Loading module hook 'hook-encodings.py' from '/usr/local/lib/python3.9/site-packages/PyInstaller/hooks'...
7510 INFO: Loading module hook 'hook-distutils.util.py' from '/usr/local/lib/python3.9/site-packages/PyInstaller/hooks'...
7513 INFO: Loading module hook 'hook-pickle.py' from '/usr/local/lib/python3.9/site-packages/PyInstaller/hooks'...
7515 INFO: Loading module hook 'hook-heapq.py' from '/usr/local/lib/python3.9/site-packages/PyInstaller/hooks'...
7517 INFO: Loading module hook 'hook-difflib.py' from '/usr/local/lib/python3.9/site-packages/PyInstaller/hooks'...
7519 INFO: Loading module hook 'hook-PyQt6.py' from '/usr/local/lib/python3.9/site-packages/PyInstaller/hooks'...
7564 INFO: Loading module hook 'hook-multiprocessing.util.py' from '/usr/local/lib/python3.9/site-packages/PyInstaller/hooks'...
7565 INFO: Loading module hook 'hook-sysconfig.py' from '/usr/local/lib/python3.9/site-packages/PyInstaller/hooks'...
7574 INFO: Loading module hook 'hook-xml.py' from '/usr/local/lib/python3.9/site-packages/PyInstaller/hooks'...
7677 INFO: Loading module hook 'hook-distutils.py' from '/usr/local/lib/python3.9/site-packages/PyInstaller/hooks'...
7694 INFO: Looking for ctypes DLLs
7712 INFO: Analyzing run-time hooks ...
7715 INFO: Including run-time hook '/usr/local/lib/python3.9/site-packages/PyInstaller/hooks/rthooks/pyi_rth_subprocess.py'
7719 INFO: Including run-time hook '/usr/local/lib/python3.9/site-packages/PyInstaller/hooks/rthooks/pyi_rth_pkgutil.py'
7722 INFO: Including run-time hook '/usr/local/lib/python3.9/site-packages/PyInstaller/hooks/rthooks/pyi_rth_multiprocessing.py'
7726 INFO: Including run-time hook '/usr/local/lib/python3.9/site-packages/PyInstaller/hooks/rthooks/pyi_rth_inspect.py'
7727 INFO: Including run-time hook '/usr/local/lib/python3.9/site-packages/PyInstaller/hooks/rthooks/pyi_rth_pyqt6.py'
7736 INFO: Looking for dynamic libraries
7977 INFO: Looking for eggs
7977 INFO: Using Python library /usr/local/Cellar/python@3.9/3.9.9/Frameworks/Python.framework/Versions/3.9/Python
7987 INFO: Warnings written to /Users/martin/app/pyqt6/build/app/warn-app.txt
8019 INFO: Graph cross-reference written to /Users/martin/app/pyqt6/build/app/xref-app.html
8032 INFO: checking PYZ
8035 INFO: Building because toc changed
8035 INFO: Building PYZ (ZlibArchive) /Users/martin/app/pyqt6/build/app/PYZ-00.pyz
8390 INFO: Building PYZ (ZlibArchive) /Users/martin/app/pyqt6/build/app/PYZ-00.pyz completed successfully.
8397 INFO: EXE target arch: x86_64
8397 INFO: Code signing identity: None
8398 INFO: checking PKG
8398 INFO: Building because /Users/martin/app/pyqt6/build/app/PYZ-00.pyz changed
8398 INFO: Building PKG (CArchive) app.pkg
8415 INFO: Building PKG (CArchive) app.pkg completed successfully.
8417 INFO: Bootloader /usr/local/lib/python3.9/site-packages/PyInstaller/bootloader/Darwin-64bit/runw
8417 INFO: checking EXE
8418 INFO: Building because console changed
8418 INFO: Building EXE from EXE-00.toc
8418 INFO: Copying bootloader EXE to /Users/martin/app/pyqt6/build/app/app
8421 INFO: Converting EXE to target arch (x86_64)
8449 INFO: Removing signature(s) from EXE
8484 INFO: Appending PKG archive to EXE
8486 INFO: Fixing EXE headers for code signing
8496 INFO: Rewriting the executable's macOS SDK version (11.1.0) to match the SDK version of the Python library (10.15.6) in order to avoid inconsistent behavior and potential UI issues in the frozen application.
8499 INFO: Re-signing the EXE
8547 INFO: Building EXE from EXE-00.toc completed successfully.
8549 INFO: checking COLLECT
WARNING: The output directory "/Users/martin/app/pyqt6/dist/app" and ALL ITS CONTENTS will be REMOVED! Continue? (y/N)y
On your own risk, you can use the option `--noconfirm` to get rid of this question.
10820 INFO: Removing dir /Users/martin/app/pyqt6/dist/app
10847 INFO: Building COLLECT COLLECT-00.toc
12460 INFO: Building COLLECT COLLECT-00.toc completed successfully.
12469 INFO: checking BUNDLE
12469 INFO: Building BUNDLE because BUNDLE-00.toc is non existent
12469 INFO: Building BUNDLE BUNDLE-00.toc
13848 INFO: Moving BUNDLE data files to Resource directory
13901 INFO: Signing the BUNDLE...
16049 INFO: Building BUNDLE BUNDLE-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 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 libcrypto.1.1.dylib
    &boxv   &boxvr&boxh&boxh PyQt6
    &boxv   ...
    &boxv   &boxvr&boxh&boxh app
    &boxv   &boxur&boxh&boxh Qt5Core
    &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 PyQt6) and binary .so files.

Since we provided the --windowed flag above, PyInstaller has actually created two builds for us. The folder app is a simple folder containing everything you need to be able to run your app. PyInstaller also creates an app bundle app.app which is what you will usually distribute to users.

The app folder is a useful debugging tool, since you can easily see the libraries and other packaged data files.

You can try running your app yourself now, either by double-clicking on the app bundle, or by running the executable file, named app.exe from the dist folder. In either case, 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 and the --windowed flag. 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=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')
app = BUNDLE(coll,
             name='app.app',
             icon=None,
             bundle_identifier=None)


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.

Because we used the --windowed command line flag, the EXE(console=) attribute is set to False. If this is True a console window will be shown when your app is launched -- not what you usually want for a GUI application.

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 app (and dist folder) by editing the .spec file to add a name= under the EXE, COLLECT and BUNDLE blocks.

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')
app = BUNDLE(coll,
             name='Hello World.app',
             icon=None,
             bundle_identifier=None)

The name under EXE is the name of the executable file, the name under BUNDLE is the name of the app bundle.

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" --windowed app.py
# or
pyinstaller --name "Hello World" --windowed app.py

The resulting app file will be given the name Hello World.app and the unpacked build 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.

Make sure you delete the old app.spec file to avoid getting confused editing the wrong one.

Application icon

By default PyInstaller app bundles come with the following icon in place.

Default PyInstaller application icon, on app bundle Default PyInstaller application icon, on app bundle

You will probably want to customize this to make your application more recognisable. This can be done easily by passing the --icon command line argument, or editing the icon= parameter of the BUNDLE section of your .spec file. For macOS app bundles you need to provide an .icns file.

python
app = BUNDLE(coll,
             name='Hello World.app',
             icon='Hello World.icns',
             bundle_identifier=None)

To create macOS icons from images you can use the image2icon tool.

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 bundle.

Custom application icon (a hand) on the app bundle Custom application icon on the app bundle

On macOS application icons are taken from the application bundle. If you repackage your app and run the bundle you will see your app icon on the dock!

Custom application icon in the dock Custom application icon on the dock

Data files and Resources

So far our application consists of just a single Python file, with no dependencies. Most real-world applications a bit more complex, and typically ship with associated data files such as icons or UI design files. In this section we'll look at how we can accomplish this with PyInstaller, starting with a single file and then bundling complete folders of resources.

First let's update our app with some more buttons and add icons to each.

python
from PyQt6.QtWidgets import QMainWindow, QApplication, QLabel, QVBoxLayout, QPushButton, QWidget
from PyQt6.QtGui import QIcon

import sys

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)

        button1 = QPushButton("Hide")
        button1.setIcon(QIcon("icons/hand.png"))
        button1.pressed.connect(self.lower)
        layout.addWidget(button1)

        button2 = QPushButton("Close")
        button2.setIcon(QIcon("icons/lightning.png"))
        button2.pressed.connect(self.close)
        layout.addWidget(button2)

        container = QWidget()
        container.setLayout(layout)

        self.setCentralWidget(container)

        self.show()

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

In the folder with this script, add a folder icons which contains two icons in PNG format, hand.png and lightning.png. You can create these yourself, or get them from the source code download for this tutorial.

Run the script now and you will see a window showing two buttons with icons.

Window with two icons Window with two buttons with icons.

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 two icons missing Window with two buttons with icons 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 it will be started with it's current working directory as the root / folder -- 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 PyQt6.QtWidgets import QMainWindow, QApplication, QLabel, QVBoxLayout, QPushButton, QWidget
from PyQt6.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)

        button1 = QPushButton("Hide")
        button1.setIcon(QIcon(os.path.join(basedir, "icons", "hand.png")))
        button1.pressed.connect(self.lower)
        layout.addWidget(button1)

        button2 = QPushButton("Close")
        button2.setIcon(QIcon(os.path.join(basedir, "icons", "lightning.png")))
        button2.pressed.connect(self.close)
        layout.addWidget(button2)

        container = QWidget()
        container.setLayout(layout)

        self.setCentralWidget(container)

        self.show()

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

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

Packaging the icons

So now we have our application showing icons, and they work wherever the application is launched from. Package the application again with pyinstaller "Hello World.spec" and then try and run it again from the dist folder as before. You'll notice the icons are missing again.

Window with two icons missing Window with two buttons with icons missing.

The problem now is that the icons haven't been copied to the dist/Hello World folder -- take a look in it. Our script expects the icons to be a specific location relative to it, and if they are not, then nothing will be shown.

This same principle applies to any other data files you package with your application, including Qt Designer UI files, settings files or source data. You need to ensure that relative path structures are replicated after packaging.

Bundling data files with PyInstaller

For the application to continue working after packaging, the files it depends on need to be in the same relative locations.

To get data files into the dist folder we can instruct PyInstaller to copy them over. PyInstaller accepts a list of individual paths to copy, 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 or in the .spec file.

Files specified on the command line are added using --add-data, passing the source file and destination folder separated by a colon :.

The path separator is platform-specific: Linux or Mac use :, on Windows use ;

bash
pyinstaller --windowed --name="Hello World" --icon="Hello World.icns" --add-data="icons/hand.png:icons" --add-data="icons/lightning.png:icons" app.py

Here we've specified the destination location as icons. The path is relative to the root of our application's folder in dist -- so dist/Hello World with our current app. The path icons means a folder named icons under this location, so dist/Hello World/icons. Putting our icons right where our application expects to find them!

You can also specify data files via the datas list in the Analysis section of the spec file, shown below.

python
a = Analysis(['app.py'],
             pathex=[],
             binaries=[],
             datas=[('icons/hand.png', 'icons'), ('icons/lightning.png', 'icons')],
             hiddenimports=[],
             hookspath=[],
             runtime_hooks=[],
             excludes=[],
             win_no_prefer_redirects=False,
             win_private_assemblies=False,
             cipher=block_cipher,
             noarchive=False)

Then rebuild from the .spec file with

bash
pyinstaller "Hello World.spec"

In both cases we are telling PyInstaller to copy the specified files to the location ./icons/ in the output folder, meaning dist/Hello World/icons. If you run the build, you should see your .png files are now in the in dist output folder, under a folder named icons.

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 icons in your window as expected!

Window with two icons Window with two buttons with icons, finally!

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.

Let's update our configuration to bundle our icons folder in one go, so it will continue to work even if we add more icons in future.

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 folder in dist.

python
# ...
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)
# ...

If you run the build using this spec file you'll see the icons folder copied across to the dist\Hello World 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.

Building the App bundle into a Disk Image

So far we've used PyInstaller to bundle the application into macOS app, along with the associated data files. The output of this bundling process is a folder and an macOS app bundle, named Hello World.app.

If you try and distribute this app bundle, you'll notice a problem: the app bundle is actually just a special folder. While macOS displays it as an application, if you try and share it, you'll actually be sharing hundreds of individual files. To distribute the app properly, we need some way to package it into a single file.

The easiest way to do this is to use a .zip file. You can zip the folder and give this to someone else to unzip on their own computer, giving them a complete app bundle they can copy to their Applications folder.

However, if you've install macOS applications before you'll know this isn't the usual way to do it. Usually you get a Disk Image .dmg file, which when opened shows the application bundle, and a link to your Applications folder. To install the app, you just drag it across to the target.

To make our app look as professional as possible, we should copy this expected behaviour. Next we'll look at how to take our app bundle and package it into a macOS Disk Image.

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 Hello World.spec file.

bash
pyinstaller "Hello World.spec"

This packages everything up as an app bundle in the dist/ folder, with a custom icon. Run the app bundle to ensure everything is bundled correctly, and you should see the same window as before with the icons visible.

Two icons Window with two icons, and a button.

Creating an Disk Image

Now we've successfully bundled our application, we'll next look at how we can take our app bundle and use it to create a macOS Disk Image for distribution.

To create our Disk Image we'll be using the create-dmg tool. This is a command-line tool which provides a simple way to build disk images automatically. If you are using Homebrew, you can install create-dmg with the following command.

bash
brew install create-dmg

...otherwise, see the Github repository for instructions.

The create-dmg tool takes a lot of options, but below are the most useful.

bash
create-dmg --help
create-dmg 1.0.9

Creates a fancy DMG file.

Usage:  create-dmg [options] <output_name.dmg> <source_folder>

All contents of <source_folder> will be copied into the disk image.

Options:
  --volname <name>
      set volume name (displayed in the Finder sidebar and window title)
  --volicon <icon.icns>
      set volume icon
  --background <pic.png>
      set folder background image (provide png, gif, or jpg)
  --window-pos <x> <y>
      set position the folder window
  --window-size <width> <height>
      set size of the folder window
  --text-size <text_size>
      set window text size (10-16)
  --icon-size <icon_size>
      set window icons size (up to 128)
  --icon file_name <x> <y>
      set position of the file's icon
  --hide-extension <file_name>
      hide the extension of file
  --app-drop-link <x> <y>
      make a drop link to Applications, at location x,y
  --no-internet-enable
      disable automatic mount & copy
  --add-file <target_name> <file>|<folder> <x> <y>
      add additional file or folder (can be used multiple times)
  -h, --help
        display this help screen

The most important thing to notice is that the command requires a <source folder> and all contents of that folder will be copied to the Disk Image. So to build the image, we first need to put our app bundle in a folder by itself.

Rather than do this manually each time you want to build a Disk Image I recommend creating a shell script. This ensures the build is reproducible, and makes it easier to configure.

Below is a working script to create a Disk Image from our app. It creates a temporary folder dist/dmg where we'll put the things we want to go in the Disk Image -- in our case, this is just the app bundle, but you can add other files if you like. Then we make sure the folder is empty (in case it still contains files from a previous run). We copy our app bundle into the folder, and finally check to see if there is already a .dmg file in dist and if so, remove it too. Then we're ready to run the create-dmg tool.

bash
#!/bin/sh
# Create a folder (named dmg) to prepare our DMG in (if it doesn't already exist).
mkdir -p dist/dmg
# Empty the dmg folder.
rm -r dist/dmg/*
# Copy the app bundle to the dmg folder.
cp -r "dist/Hello World.app" dist/dmg
# If the DMG already exists, delete it.
test -f "dist/Hello World.dmg" && rm "dist/Hello World.dmg"
create-dmg \
  --volname "Hello World" \
  --volicon "Hello World.icns" \
  --window-pos 200 120 \
  --window-size 600 300 \
  --icon-size 100 \
  --icon "Hello World.app" 175 120 \
  --hide-extension "Hello World.app" \
  --app-drop-link 425 120 \
  "dist/Hello World.dmg" \
  "dist/dmg/"

The options we pass to create-dmg set the dimensions of the Disk Image window when it is opened, and positions of the icons in it.

Save this shell script in the root of your project, named e.g. builddmg.sh. To make it possible to run, you need to set the execute bit with.

bash
chmod +x builddmg.sh

With that, you can now build a Disk Image for your Hello World app with the command.

bash
./builddmg.sh

This will take a few seconds to run, producing quite a bit of output.

bash
 No such file or directory
Creating disk image...
...............................................................
created: /Users/martin/app/dist/rw.Hello World.dmg
Mounting disk image...
Mount directory: /Volumes/Hello World
Device name:     /dev/disk2
Making link to Applications dir...
/Volumes/Hello World
Copying volume icon file 'Hello World.icns'...
Running AppleScript to make Finder stuff pretty: /usr/bin/osascript "/var/folders/yf/1qvxtg4d0vz6h2y4czd69tf40000gn/T/createdmg.tmp.XXXXXXXXXX.RvPoqdr0" "Hello World"
waited 1 seconds for .DS_STORE to be created.
Done running the AppleScript...
Fixing permissions...
Done fixing permissions
Blessing started
Blessing finished
Deleting .fseventsd
Unmounting disk image...
hdiutil: couldn't unmount "disk2" - Resource busy
Wait a moment...
Unmounting disk image...
"disk2" ejected.
Compressing disk image...
Preparing imaging engine…
Reading Protective Master Boot Record (MBR : 0)…
   (CRC32 $38FC6E30: Protective Master Boot Record (MBR : 0))
Reading GPT Header (Primary GPT Header : 1)…
   (CRC32 $59C36109: GPT Header (Primary GPT Header : 1))
Reading GPT Partition Data (Primary GPT Table : 2)…
   (CRC32 $528491DC: GPT Partition Data (Primary GPT Table : 2))
Reading  (Apple_Free : 3)…
   (CRC32 $00000000:  (Apple_Free : 3))
Reading disk image (Apple_HFS : 4)…
...............................................................................
   (CRC32 $FCDC1017: disk image (Apple_HFS : 4))
Reading  (Apple_Free : 5)…
...............................................................................
   (CRC32 $00000000:  (Apple_Free : 5))
Reading GPT Partition Data (Backup GPT Table : 6)…
...............................................................................
   (CRC32 $528491DC: GPT Partition Data (Backup GPT Table : 6))
Reading GPT Header (Backup GPT Header : 7)…
...............................................................................
   (CRC32 $56306308: GPT Header (Backup GPT Header : 7))
Adding resources…
...............................................................................
Elapsed Time:  3.443s
File size: 23178950 bytes, Checksum: CRC32 $141F3DDC
Sectors processed: 184400, 131460 compressed
Speed: 18.6Mbytes/sec
Savings: 75.4%
created: /Users/martin/app/dist/Hello World.dmg
hdiutil does not support internet-enable. Note it was removed in macOS 10.15.
Disk image done

While it's building, the Disk Image will pop up. Don't get too excited yet, it's still building. Wait for the script to complete, and you will find the finished .dmg file in the dist/ folder.

The Disk Image in the dist folder The Disk Image created in the dist folder

Running the installer

Double-click the Disk Image to open it, and you'll see the usual macOS install view. Click and drag your app across the the Applications folder to install it.

The Disk Image containing your file The Disk Image contains the app bundle and a shortcut to the applications folder

If you open the Showcase view (press F4) you will see your app installed. If you have a lot of apps, you can search for it by typing "Hello"

The app is installed! The app installed on macOS

Repeating the build

Now you have everything set up, you can create a new app bundle & Disk Image of your application any time, by running the two commands from the command line.

bash
pyinstaller "Hello World.spec"
./builddmg.sh

It's that simple!

Wrapping up

In this tutorial we've covered how to build your PyQt6 applications into a macOS app bundle using PyInstaller, including adding data files along with your code. Then we walked through the process of creating a Disk Image 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.

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

Packaging PySide6 applications into a macOS app with PyInstaller

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 PySide6. 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 more complex PySide6 applications into distributable macOS app bundles. 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 a macOS Disk Image, the usual method for distributing applications on macOS.

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 Disk Image for macOS Example Disk Image Installer for macOS

If you're impatient, you can download the Example Disk Image for macOS first.

Requirements

PyInstaller works out of the box with PySide6 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 PySide6 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 PySide6 you would use —

python
pip3 install PySide6 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 PySide6 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 macOS).

Simple skeleton app in PySide6 Simple skeleton app in PySide6

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 --windowed app.py

The --windowed flag is necessary to tell PyInstaller to build a macOS .app bundle.

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
martin@MacBook-Pro pyside6 % pyinstaller --windowed app.py
74 INFO: PyInstaller: 4.8
74 INFO: Python: 3.9.9
83 INFO: Platform: macOS-10.15.7-x86_64-i386-64bit
84 INFO: wrote /Users/martin/app/pyside6/app.spec
87 INFO: UPX is not available.
88 INFO: Extending PYTHONPATH with paths
['/Users/martin/app/pyside6']
447 INFO: checking Analysis
451 INFO: Building because inputs changed
452 INFO: Initializing module dependency graph...
455 INFO: Caching module graph hooks...
463 INFO: Analyzing base_library.zip ...
3914 INFO: Processing pre-find module path hook distutils from '/usr/local/lib/python3.9/site-packages/PyInstaller/hooks/pre_find_module_path/hook-distutils.py'.
3917 INFO: distutils: retargeting to non-venv dir '/usr/local/Cellar/python@3.9/3.9.9/Frameworks/Python.framework/Versions/3.9/lib/python3.9'
6928 INFO: Caching module dependency graph...
7083 INFO: running Analysis Analysis-00.toc
7091 INFO: Analyzing /Users/martin/app/pyside6/app.py
7138 INFO: Processing module hooks...
7139 INFO: Loading module hook 'hook-PyQt6.QtWidgets.py' from '/usr/local/lib/python3.9/site-packages/PyInstaller/hooks'...
7336 INFO: Loading module hook 'hook-xml.etree.cElementTree.py' from '/usr/local/lib/python3.9/site-packages/PyInstaller/hooks'...
7337 INFO: Loading module hook 'hook-lib2to3.py' from '/usr/local/lib/python3.9/site-packages/PyInstaller/hooks'...
7360 INFO: Loading module hook 'hook-PyQt6.QtGui.py' from '/usr/local/lib/python3.9/site-packages/PyInstaller/hooks'...
7397 INFO: Loading module hook 'hook-PyQt6.QtCore.py' from '/usr/local/lib/python3.9/site-packages/PyInstaller/hooks'...
7422 INFO: Loading module hook 'hook-encodings.py' from '/usr/local/lib/python3.9/site-packages/PyInstaller/hooks'...
7510 INFO: Loading module hook 'hook-distutils.util.py' from '/usr/local/lib/python3.9/site-packages/PyInstaller/hooks'...
7513 INFO: Loading module hook 'hook-pickle.py' from '/usr/local/lib/python3.9/site-packages/PyInstaller/hooks'...
7515 INFO: Loading module hook 'hook-heapq.py' from '/usr/local/lib/python3.9/site-packages/PyInstaller/hooks'...
7517 INFO: Loading module hook 'hook-difflib.py' from '/usr/local/lib/python3.9/site-packages/PyInstaller/hooks'...
7519 INFO: Loading module hook 'hook-PyQt6.py' from '/usr/local/lib/python3.9/site-packages/PyInstaller/hooks'...
7564 INFO: Loading module hook 'hook-multiprocessing.util.py' from '/usr/local/lib/python3.9/site-packages/PyInstaller/hooks'...
7565 INFO: Loading module hook 'hook-sysconfig.py' from '/usr/local/lib/python3.9/site-packages/PyInstaller/hooks'...
7574 INFO: Loading module hook 'hook-xml.py' from '/usr/local/lib/python3.9/site-packages/PyInstaller/hooks'...
7677 INFO: Loading module hook 'hook-distutils.py' from '/usr/local/lib/python3.9/site-packages/PyInstaller/hooks'...
7694 INFO: Looking for ctypes DLLs
7712 INFO: Analyzing run-time hooks ...
7715 INFO: Including run-time hook '/usr/local/lib/python3.9/site-packages/PyInstaller/hooks/rthooks/pyi_rth_subprocess.py'
7719 INFO: Including run-time hook '/usr/local/lib/python3.9/site-packages/PyInstaller/hooks/rthooks/pyi_rth_pkgutil.py'
7722 INFO: Including run-time hook '/usr/local/lib/python3.9/site-packages/PyInstaller/hooks/rthooks/pyi_rth_multiprocessing.py'
7726 INFO: Including run-time hook '/usr/local/lib/python3.9/site-packages/PyInstaller/hooks/rthooks/pyi_rth_inspect.py'
7727 INFO: Including run-time hook '/usr/local/lib/python3.9/site-packages/PyInstaller/hooks/rthooks/pyi_rth_pyqt6.py'
7736 INFO: Looking for dynamic libraries
7977 INFO: Looking for eggs
7977 INFO: Using Python library /usr/local/Cellar/python@3.9/3.9.9/Frameworks/Python.framework/Versions/3.9/Python
7987 INFO: Warnings written to /Users/martin/app/pyside6/build/app/warn-app.txt
8019 INFO: Graph cross-reference written to /Users/martin/app/pyside6/build/app/xref-app.html
8032 INFO: checking PYZ
8035 INFO: Building because toc changed
8035 INFO: Building PYZ (ZlibArchive) /Users/martin/app/pyside6/build/app/PYZ-00.pyz
8390 INFO: Building PYZ (ZlibArchive) /Users/martin/app/pyside6/build/app/PYZ-00.pyz completed successfully.
8397 INFO: EXE target arch: x86_64
8397 INFO: Code signing identity: None
8398 INFO: checking PKG
8398 INFO: Building because /Users/martin/app/pyside6/build/app/PYZ-00.pyz changed
8398 INFO: Building PKG (CArchive) app.pkg
8415 INFO: Building PKG (CArchive) app.pkg completed successfully.
8417 INFO: Bootloader /usr/local/lib/python3.9/site-packages/PyInstaller/bootloader/Darwin-64bit/runw
8417 INFO: checking EXE
8418 INFO: Building because console changed
8418 INFO: Building EXE from EXE-00.toc
8418 INFO: Copying bootloader EXE to /Users/martin/app/pyside6/build/app/app
8421 INFO: Converting EXE to target arch (x86_64)
8449 INFO: Removing signature(s) from EXE
8484 INFO: Appending PKG archive to EXE
8486 INFO: Fixing EXE headers for code signing
8496 INFO: Rewriting the executable's macOS SDK version (11.1.0) to match the SDK version of the Python library (10.15.6) in order to avoid inconsistent behavior and potential UI issues in the frozen application.
8499 INFO: Re-signing the EXE
8547 INFO: Building EXE from EXE-00.toc completed successfully.
8549 INFO: checking COLLECT
WARNING: The output directory "/Users/martin/app/pyside6/dist/app" and ALL ITS CONTENTS will be REMOVED! Continue? (y/N)y
On your own risk, you can use the option `--noconfirm` to get rid of this question.
10820 INFO: Removing dir /Users/martin/app/pyside6/dist/app
10847 INFO: Building COLLECT COLLECT-00.toc
12460 INFO: Building COLLECT COLLECT-00.toc completed successfully.
12469 INFO: checking BUNDLE
12469 INFO: Building BUNDLE because BUNDLE-00.toc is non existent
12469 INFO: Building BUNDLE BUNDLE-00.toc
13848 INFO: Moving BUNDLE data files to Resource directory
13901 INFO: Signing the BUNDLE...
16049 INFO: Building BUNDLE BUNDLE-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 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 libcrypto.1.1.dylib
    &boxv   &boxvr&boxh&boxh PySide6
    &boxv   ...
    &boxv   &boxvr&boxh&boxh app
    &boxv   &boxur&boxh&boxh Qt5Core
    &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 PySide6) and binary .so files.

Since we provided the --windowed flag above, PyInstaller has actually created two builds for us. The folder app is a simple folder containing everything you need to be able to run your app. PyInstaller also creates an app bundle app.app which is what you will usually distribute to users.

The app folder is a useful debugging tool, since you can easily see the libraries and other packaged data files.

You can try running your app yourself now, either by double-clicking on the app bundle, or by running the executable file, named app.exe from the dist folder. In either case, 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 and the --windowed flag. 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=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')
app = BUNDLE(coll,
             name='app.app',
             icon=None,
             bundle_identifier=None)


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.

Because we used the --windowed command line flag, the EXE(console=) attribute is set to False. If this is True a console window will be shown when your app is launched -- not what you usually want for a GUI application.

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 app (and dist folder) by editing the .spec file to add a name= under the EXE, COLLECT and BUNDLE blocks.

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')
app = BUNDLE(coll,
             name='Hello World.app',
             icon=None,
             bundle_identifier=None)

The name under EXE is the name of the executable file, the name under BUNDLE is the name of the app bundle.

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" --windowed app.py
# or
pyinstaller --name "Hello World" --windowed app.py

The resulting app file will be given the name Hello World.app and the unpacked build 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.

Make sure you delete the old app.spec file to avoid getting confused editing the wrong one.

Application icon

By default PyInstaller app bundles come with the following icon in place.

Default PyInstaller application icon, on app bundle Default PyInstaller application icon, on app bundle

You will probably want to customize this to make your application more recognisable. This can be done easily by passing the --icon command line argument, or editing the icon= parameter of the BUNDLE section of your .spec file. For macOS app bundles you need to provide an .icns file.

python
app = BUNDLE(coll,
             name='Hello World.app',
             icon='Hello World.icns',
             bundle_identifier=None)

To create macOS icons from images you can use the image2icon tool.

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 bundle.

Custom application icon (a hand) on the app bundle Custom application icon on the app bundle

On macOS application icons are taken from the application bundle. If you repackage your app and run the bundle you will see your app icon on the dock!

Custom application icon in the dock Custom application icon on the dock

Data files and Resources

So far our application consists of just a single Python file, with no dependencies. Most real-world applications a bit more complex, and typically ship with associated data files such as icons or UI design files. In this section we'll look at how we can accomplish this with PyInstaller, starting with a single file and then bundling complete folders of resources.

First let's update our app with some more buttons and add icons to each.

python
from PySide6.QtWidgets import QMainWindow, QApplication, QLabel, QVBoxLayout, QPushButton, QWidget
from PySide6.QtGui import QIcon

import sys

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)

        button1 = QPushButton("Hide")
        button1.setIcon(QIcon("icons/hand.png"))
        button1.pressed.connect(self.lower)
        layout.addWidget(button1)

        button2 = QPushButton("Close")
        button2.setIcon(QIcon("icons/lightning.png"))
        button2.pressed.connect(self.close)
        layout.addWidget(button2)

        container = QWidget()
        container.setLayout(layout)

        self.setCentralWidget(container)

        self.show()

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

In the folder with this script, add a folder icons which contains two icons in PNG format, hand.png and lightning.png. You can create these yourself, or get them from the source code download for this tutorial.

Run the script now and you will see a window showing two buttons with icons.

Window with two icons Window with two buttons with icons.

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 two icons missing Window with two buttons with icons 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 it will be started with it's current working directory as the root / folder -- 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 PySide6.QtWidgets import QMainWindow, QApplication, QLabel, QVBoxLayout, QPushButton, QWidget
from PySide6.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)

        button1 = QPushButton("Hide")
        button1.setIcon(QIcon(os.path.join(basedir, "icons", "hand.png")))
        button1.pressed.connect(self.lower)
        layout.addWidget(button1)

        button2 = QPushButton("Close")
        button2.setIcon(QIcon(os.path.join(basedir, "icons", "lightning.png")))
        button2.pressed.connect(self.close)
        layout.addWidget(button2)

        container = QWidget()
        container.setLayout(layout)

        self.setCentralWidget(container)

        self.show()

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

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

Packaging the icons

So now we have our application showing icons, and they work wherever the application is launched from. Package the application again with pyinstaller "Hello World.spec" and then try and run it again from the dist folder as before. You'll notice the icons are missing again.

Window with two icons missing Window with two buttons with icons missing.

The problem now is that the icons haven't been copied to the dist/Hello World folder -- take a look in it. Our script expects the icons to be a specific location relative to it, and if they are not, then nothing will be shown.

This same principle applies to any other data files you package with your application, including Qt Designer UI files, settings files or source data. You need to ensure that relative path structures are replicated after packaging.

Bundling data files with PyInstaller

For the application to continue working after packaging, the files it depends on need to be in the same relative locations.

To get data files into the dist folder we can instruct PyInstaller to copy them over. PyInstaller accepts a list of individual paths to copy, 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 or in the .spec file.

Files specified on the command line are added using --add-data, passing the source file and destination folder separated by a colon :.

The path separator is platform-specific: Linux or Mac use :, on Windows use ;

bash
pyinstaller --windowed --name="Hello World" --icon="Hello World.icns" --add-data="icons/hand.png:icons" --add-data="icons/lightning.png:icons" app.py

Here we've specified the destination location as icons. The path is relative to the root of our application's folder in dist -- so dist/Hello World with our current app. The path icons means a folder named icons under this location, so dist/Hello World/icons. Putting our icons right where our application expects to find them!

You can also specify data files via the datas list in the Analysis section of the spec file, shown below.

python
a = Analysis(['app.py'],
             pathex=[],
             binaries=[],
             datas=[('icons/hand.png', 'icons'), ('icons/lightning.png', 'icons')],
             hiddenimports=[],
             hookspath=[],
             runtime_hooks=[],
             excludes=[],
             win_no_prefer_redirects=False,
             win_private_assemblies=False,
             cipher=block_cipher,
             noarchive=False)

Then rebuild from the .spec file with

bash
pyinstaller "Hello World.spec"

In both cases we are telling PyInstaller to copy the specified files to the location ./icons/ in the output folder, meaning dist/Hello World/icons. If you run the build, you should see your .png files are now in the in dist output folder, under a folder named icons.

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 icons in your window as expected!

Window with two icons Window with two buttons with icons, finally!

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.

Let's update our configuration to bundle our icons folder in one go, so it will continue to work even if we add more icons in future.

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 folder in dist.

python
# ...
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)
# ...

If you run the build using this spec file you'll see the icons folder copied across to the dist\Hello World 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.

Building the App bundle into a Disk Image

So far we've used PyInstaller to bundle the application into macOS app, along with the associated data files. The output of this bundling process is a folder and an macOS app bundle, named Hello World.app.

If you try and distribute this app bundle, you'll notice a problem: the app bundle is actually just a special folder. While macOS displays it as an application, if you try and share it, you'll actually be sharing hundreds of individual files. To distribute the app properly, we need some way to package it into a single file.

The easiest way to do this is to use a .zip file. You can zip the folder and give this to someone else to unzip on their own computer, giving them a complete app bundle they can copy to their Applications folder.

However, if you've install macOS applications before you'll know this isn't the usual way to do it. Usually you get a Disk Image .dmg file, which when opened shows the application bundle, and a link to your Applications folder. To install the app, you just drag it across to the target.

To make our app look as professional as possible, we should copy this expected behaviour. Next we'll look at how to take our app bundle and package it into a macOS Disk Image.

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 Hello World.spec file.

bash
pyinstaller "Hello World.spec"

This packages everything up as an app bundle in the dist/ folder, with a custom icon. Run the app bundle to ensure everything is bundled correctly, and you should see the same window as before with the icons visible.

Two icons Window with two icons, and a button.

Creating an Disk Image

Now we've successfully bundled our application, we'll next look at how we can take our app bundle and use it to create a macOS Disk Image for distribution.

To create our Disk Image we'll be using the create-dmg tool. This is a command-line tool which provides a simple way to build disk images automatically. If you are using Homebrew, you can install create-dmg with the following command.

bash
brew install create-dmg

...otherwise, see the Github repository for instructions.

The create-dmg tool takes a lot of options, but below are the most useful.

bash
create-dmg --help
create-dmg 1.0.9

Creates a fancy DMG file.

Usage:  create-dmg [options] <output_name.dmg> <source_folder>

All contents of <source_folder> will be copied into the disk image.

Options:
  --volname <name>
      set volume name (displayed in the Finder sidebar and window title)
  --volicon <icon.icns>
      set volume icon
  --background <pic.png>
      set folder background image (provide png, gif, or jpg)
  --window-pos <x> <y>
      set position the folder window
  --window-size <width> <height>
      set size of the folder window
  --text-size <text_size>
      set window text size (10-16)
  --icon-size <icon_size>
      set window icons size (up to 128)
  --icon file_name <x> <y>
      set position of the file's icon
  --hide-extension <file_name>
      hide the extension of file
  --app-drop-link <x> <y>
      make a drop link to Applications, at location x,y
  --no-internet-enable
      disable automatic mount & copy
  --add-file <target_name> <file>|<folder> <x> <y>
      add additional file or folder (can be used multiple times)
  -h, --help
        display this help screen

The most important thing to notice is that the command requires a <source folder> and all contents of that folder will be copied to the Disk Image. So to build the image, we first need to put our app bundle in a folder by itself.

Rather than do this manually each time you want to build a Disk Image I recommend creating a shell script. This ensures the build is reproducible, and makes it easier to configure.

Below is a working script to create a Disk Image from our app. It creates a temporary folder dist/dmg where we'll put the things we want to go in the Disk Image -- in our case, this is just the app bundle, but you can add other files if you like. Then we make sure the folder is empty (in case it still contains files from a previous run). We copy our app bundle into the folder, and finally check to see if there is already a .dmg file in dist and if so, remove it too. Then we're ready to run the create-dmg tool.

bash
#!/bin/sh
# Create a folder (named dmg) to prepare our DMG in (if it doesn't already exist).
mkdir -p dist/dmg
# Empty the dmg folder.
rm -r dist/dmg/*
# Copy the app bundle to the dmg folder.
cp -r "dist/Hello World.app" dist/dmg
# If the DMG already exists, delete it.
test -f "dist/Hello World.dmg" && rm "dist/Hello World.dmg"
create-dmg \
  --volname "Hello World" \
  --volicon "Hello World.icns" \
  --window-pos 200 120 \
  --window-size 600 300 \
  --icon-size 100 \
  --icon "Hello World.app" 175 120 \
  --hide-extension "Hello World.app" \
  --app-drop-link 425 120 \
  "dist/Hello World.dmg" \
  "dist/dmg/"

The options we pass to create-dmg set the dimensions of the Disk Image window when it is opened, and positions of the icons in it.

Save this shell script in the root of your project, named e.g. builddmg.sh. To make it possible to run, you need to set the execute bit with.

bash
chmod +x builddmg.sh

With that, you can now build a Disk Image for your Hello World app with the command.

bash
./builddmg.sh

This will take a few seconds to run, producing quite a bit of output.

bash
 No such file or directory
Creating disk image...
...............................................................
created: /Users/martin/app/dist/rw.Hello World.dmg
Mounting disk image...
Mount directory: /Volumes/Hello World
Device name:     /dev/disk2
Making link to Applications dir...
/Volumes/Hello World
Copying volume icon file 'Hello World.icns'...
Running AppleScript to make Finder stuff pretty: /usr/bin/osascript "/var/folders/yf/1qvxtg4d0vz6h2y4czd69tf40000gn/T/createdmg.tmp.XXXXXXXXXX.RvPoqdr0" "Hello World"
waited 1 seconds for .DS_STORE to be created.
Done running the AppleScript...
Fixing permissions...
Done fixing permissions
Blessing started
Blessing finished
Deleting .fseventsd
Unmounting disk image...
hdiutil: couldn't unmount "disk2" - Resource busy
Wait a moment...
Unmounting disk image...
"disk2" ejected.
Compressing disk image...
Preparing imaging engine…
Reading Protective Master Boot Record (MBR : 0)…
   (CRC32 $38FC6E30: Protective Master Boot Record (MBR : 0))
Reading GPT Header (Primary GPT Header : 1)…
   (CRC32 $59C36109: GPT Header (Primary GPT Header : 1))
Reading GPT Partition Data (Primary GPT Table : 2)…
   (CRC32 $528491DC: GPT Partition Data (Primary GPT Table : 2))
Reading  (Apple_Free : 3)…
   (CRC32 $00000000:  (Apple_Free : 3))
Reading disk image (Apple_HFS : 4)…
...............................................................................
   (CRC32 $FCDC1017: disk image (Apple_HFS : 4))
Reading  (Apple_Free : 5)…
...............................................................................
   (CRC32 $00000000:  (Apple_Free : 5))
Reading GPT Partition Data (Backup GPT Table : 6)…
...............................................................................
   (CRC32 $528491DC: GPT Partition Data (Backup GPT Table : 6))
Reading GPT Header (Backup GPT Header : 7)…
...............................................................................
   (CRC32 $56306308: GPT Header (Backup GPT Header : 7))
Adding resources…
...............................................................................
Elapsed Time:  3.443s
File size: 23178950 bytes, Checksum: CRC32 $141F3DDC
Sectors processed: 184400, 131460 compressed
Speed: 18.6Mbytes/sec
Savings: 75.4%
created: /Users/martin/app/dist/Hello World.dmg
hdiutil does not support internet-enable. Note it was removed in macOS 10.15.
Disk image done

While it's building, the Disk Image will pop up. Don't get too excited yet, it's still building. Wait for the script to complete, and you will find the finished .dmg file in the dist/ folder.

The Disk Image in the dist folder The Disk Image created in the dist folder

Running the installer

Double-click the Disk Image to open it, and you'll see the usual macOS install view. Click and drag your app across the the Applications folder to install it.

The Disk Image containing your file The Disk Image contains the app bundle and a shortcut to the applications folder

If you open the Showcase view (press F4) you will see your app installed. If you have a lot of apps, you can search for it by typing "Hello"

The app is installed! The app installed on macOS

Repeating the build

Now you have everything set up, you can create a new app bundle & Disk Image of your application any time, by running the two commands from the command line.

bash
pyinstaller "Hello World.spec"
./builddmg.sh

It's that simple!

Wrapping up

In this tutorial we've covered how to build your PySide6 applications into a macOS app bundle using PyInstaller, including adding data files along with your code. Then we walked through the process of creating a Disk Image 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.

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