Professional Documents
Culture Documents
PyInstaller
The hands-on guide to distributable Python apps
Martin Fitzpatrick
1
Getting Started with PyInstaller
There is not much fun in creating your own application if you can’t share it
with other people — whether that 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!
2
1. What is PyInstaller?
PyInstaller is a cross-platform packaging system which supports building
desktop applications for Windows, macOS and Linux. It automatically
handles packaging of your Python applications, along with any associated
libraries and data files, either into a standalone one-file executable or a
distributable folder you can then use to create an installer.
3
2. Installing PyInstaller
PyInstaller works out of the box with all of the common Python GUI libraries
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.
Virtual Environments
Python Virtual Environments allow you to create a self-contained Python
installation where you can install packages without affecting your other
installations. This is a good idea, because your projects may end up
depending on different specific versions of a library, and no longer work if
these change. By creating a virtual environment you can isolate your
different projects and upgrade things in a controlled way.
4
The second argument env is the location to create the environment — here
we’re creating an environment in a folder called env in our projects folder.
To start working with your new environment, you’ll need to activate it.
When you activate a virtual environment the environment’s Python and
executables are put at the front of your shell’s PATH variable — meaning they
will be loaded in preference to any other Python installation.
On Linux or macOS:
source env/bin/activate
Or on Windows:
.\env\Scripts\activate
With the virtual environment activated you can use pip as normally to
install packages into the environment as normal. These packages can be
imported and used by any applications run using this Python environment.
If you run your GUI applications with the environment activated, they will
use this python executable. Packages created with PyInstaller will bundle
packages from the environment.
You can exit an active virtual environment — for example, you switch
projects — by running deactivate:
5
deactivate
You can activate the virtual environment again using `activate ` — you don’t
need to recreate the environment again.
If you experience problems packaging your apps, your first step should
always be to update your PyInstaller and hooks packages to the latest versions
using:
6
3. My first app
Before we can start building an application package, we need an
application! To begin with we’re going to start with the simplest, most bare-
bones application imaginable — a single window, with no icons or other
data files. By testing this basic example first you can be sure that the
underlying systems are all working. We can then move onto more
interesting things!
There are a huge number of different Python GUI frameworks which you
may be using to develop your apps. I use PyQt almost exclusively, but you
may not — and it’s easier to follow code if you’re already familiar with the
libraries being used. For that reason, I’ve included example code for 5 of
the most popular Python GUI libraries below — PyQt6, Tkinter, wxPython,
PySimpleGUI and Kivy — throughout the book when introducing new
examples. The source code downloads with the book also include examples
for PyQt5, PySide6 and PySide2 — giving you 8 options in total.
7
Your first application
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 start extending things, confirming the build is still working at
each step.
To start with add the following Python code in a file named app.py in your
project’s folder (where we created the virtual environment).
http://www.pythonguis.com/d/python-packaging-source.zip
— download it now & save yourself some typing!
8
PyQt6
Listing 1. pyqt6/basic/app.py
import sys
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Hello World")
self.setCentralWidget(button)
self.show()
app = QApplication(sys.argv)
w = MainWindow()
app.exec()
9
Tkinter
Listing 2. tkinter/basic/app.py
import tkinter as tk
window = tk.Tk()
window.title("Hello World")
def handle_button_press(event):
window.destroy()
10
wxPython
Listing 3. wxpython/basic/app.py
import wx
class MainWindow(wx.Frame):
def __init__(self, parent, title):
wx.Frame.__init__(self, parent, title=title, size=(200, -1))
self.sizer = wx.BoxSizer(wx.VERTICAL)
self.sizer.Add(self.button)
self.SetSizer(self.sizer)
self.SetAutoLayout(True)
self.Show()
app = wx.App(False)
w = MainWindow(None, "Hello World")
app.MainLoop()
11
PySimpleGUI
Listing 4. pysimplegui/basic/app.py
import PySimpleGUI as sg
while True:
event, values = window.read()
print(event, values)
if event == sg.WIN_CLOSED or event == "My simple app.":
break
window.close()
12
Kivy
Listing 5. kivy/basic/app.py
class MainWindow(BoxLayout):
def __init__(self):
super().__init__()
button = Button(text="My simple app.")
button.bind(on_press=self.handle_button_clicked)
self.add_widget(button)
class MyApp(App):
def build(self):
self.title = "Hello World"
return MainWindow()
app = MyApp()
app.run()
13
This is a basic bare-bones application which creates a single window with a
pushbutton in it. Pressing the button will close the window. You can run
this app as follows:
python app.py
This should produce one of the following windows (depending on your GUI
library).
Figure 1. Simple app running in PyQt6, Tkinter, wxPythn, Kivy & PySimpleGUI
14
pyinstaller app.py
After the build is complete, look in your folder and you’ll notice you now
have two new folders dist and build.
Below is a truncated listing of the folder structure showing the build and
dist folders. The actual files will differ depending on which platform you’re
building on, but the general structure is always the same.
15
.
├── app.py
├── app.spec
├── build
│ └── app
│ ├── localpycos
│ ├── Analysis-00.toc
│ ├── COLLECT-00.toc
│ ├── EXE-00.toc
│ ├── PKG-00.pkg
│ ├── PKG-00.toc
│ ├── PYZ-00.pyz
│ ├── PYZ-00.toc
│ ├── app
│ ├── app.pkg
│ ├── base_library.zip
│ ├── warn-app.txt
│ └── xref-app.html
└── dist
└── app
├── lib-dynload
...
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 the GUI library).
You can try running your built app yourself now, by running the executable
16
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.
If you’ve confirmed that the build works, try again but add the --windowed
flag. The --windowed command line option is required to build a .app
application bundle on macOS and to hide the terminal output window on
Windows. On Linux it has no effect.
17
Troubleshooting builds.
In the same folder as your Python file, alongside the build and dist folders
PyInstaller will have also created a .spec file.
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.
Listing 7. common/basic/app.spec
block_cipher = None
a = Analysis(['app.py'],
pathex=[],
binaries=[],
datas=[],
hiddenimports=[],
18
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.
If you’re building on macOS and passed the --windowed flag you’ll also have
19
an additional BUNDLE block, which is used to build the .app bundle. That
section will look something like this:
app = BUNDLE(coll,
name='app.app',
icon=None,
bundle_identifier=None)
If you’re starting your build on another platform, but want to target macOS
later you can add this to the end of your .spec file manually. You can also
toggle the Windows console output by editing console=True to
console=False in the .spec file — this is the equivalent of the --windowed
flag.
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.
pyinstaller app.spec
The resulting build will be identical to the build used to generate the .spec
file (assuming you have made no changes to your project). 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, although I would recommend editing the .spec file for more
complex builds.
20
Tweaking your Build
So far we’ve created the simplest application possible & successfully
packaged it with PyInstaller. The packaged version of the application runs as
expected.
In this chapter we’ll take this simple example and start expanding it, step by
step. We’ll customize our PyInstaller build, adding additional dependencies
on data files, and looking at how to handle problems with 3rd party
libraries. By the end of this chapter you’ll be able to package real
applications.
21
4. 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 to name the executable.
You can provide a nicer name for PyInstaller to use for your executable file
(and dist folder) by editing the .spec file and changing the name= under the
EXE and COLLECT blocks (and BUNDLE on macOS).
Listing 8. common/custom/hello-world.spec
exe = EXE(pyz,
a.scripts,
[],
exclude_binaries=True,
name='hello-world',
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='hello-world')
The name under EXE is the name of the executable file while the name under
COLLECT is the name of the output folder.
22
I’d recommend you to use a name with no spaces for the
executable — use hyphens or CamelCase instead.
The name specified in the BUNDLE block is used for the macOS app bundle,
which is the user-visible name of the application shown in Launchpad and
on the dock. In our example we’ve called our application executable "hello-
world", but for the .app bundle you can use the more friendly "Hello
World.app".
Listing 9. common/custom/hello-world.spec
app = BUNDLE(coll,
name='Hello World.app',
icon=None,
bundle_identifier=None)
Alternatively, you can re-run the pyinstaller command and pass the -n or
--name configuration flag along with your app.py script.
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.
23
Figure 4. Application with custom name "hello-world".
24
5. Application icons
One of the first things you will want to do is add an icon to your application.
This is a bit more complicated than it sounds, particularly if you’re targeting
multiple platforms. Despite the similarities that exist elsewhere, for some
reason the one thing OS designers have been unable to agree on is how to
specify window icons. Windows uses ICO files, macOS ICNS, Linux uses
multiple PNG or SVG files. Even worse, some GUI libraries have different
requirements depending on what you’re using the icons for.
It’s a mess. But we’ll step through the important bits here. The good news is
that once you’ve got your project working, you won’t need to touch it again.
25
A note about icons.
The icon files you can use will vary depending on the GUI
library you’re using and the platform(s) you’re working
with.
Window Icons
We can set the application icon used for the window/dock directly from the
source code. Where the icons appear will depend on your current platform
— Windows will show the icons on the window and (sometimes!) the
taskbar, macOS will show the icons on the Dock and on Linux the icon will
be used in the dock/taskbar depending on your window manager.
Below are examples for setting the window icon for each of the different
GUI frameworks.
26
PyQt6
import sys
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Hello World")
self.setCentralWidget(button)
self.show()
app = QApplication(sys.argv)
app.setWindowIcon(QIcon("icon.svg"))
w = MainWindow()
app.exec()
27
Tkinter
import tkinter as tk
window = tk.Tk()
window.title("Hello World")
def handle_button_press(event):
window.destroy()
28
wxPython
import wx
class MainWindow(wx.Frame):
def __init__(self, parent, title):
wx.Frame.__init__(self, parent, title=title, size=(200, -1))
self.sizer = wx.BoxSizer(wx.VERTICAL)
self.sizer.Add(self.button)
self.SetSizer(self.sizer)
self.SetAutoLayout(True)
self.Show()
app = wx.App(False)
w = MainWindow(None, "Hello World")
w.SetIcon(wx.Icon("icon.ico"))
app.MainLoop()
29
PySimpleGUI
import PySimpleGUI as sg
while True:
event, values = window.read()
print(event, values)
if event == sg.WIN_CLOSED or event == "My simple app.":
break
window.close()
30
Kivy
class MainWindow(BoxLayout):
def __init__(self):
super().__init__()
button = Button(text="My simple app.")
button.bind(on_press=self.handle_button_clicked)
self.add_widget(button)
class MyApp(App):
def build(self):
self.title = "Hello World"
self.icon = "icon.png"
return MainWindow()
app = MyApp()
app.run()
31
If you run the above application you should now see the icon appears on the
window on Windows and on the dock in macOS or Ubuntu Linux.
If you don’t see the icon, make sure you are running the
script from the same folder. Open a shell and cd to the
folder with your script, then run it with python app.py.
We’ll address this issue in the next chapter!
If it does for you, great! But it may not work when you
distribute your application, so follow the next steps anyway!
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 executed by python.exe (which includes 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 for our app.
32
PyQt6
import os
import sys
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
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Hello World")
button = QPushButton("My simple app.")
button.setIcon(QIcon(os.path.join(basedir, "icon.svg")))
button.pressed.connect(self.close)
self.setCentralWidget(button)
self.show()
app = QApplication(sys.argv)
app.setWindowIcon(QIcon(os.path.join(basedir, "icon.svg")))
w = MainWindow()
app.exec()
① The code is wrapped in a try/except block since the windll module is not
available on macOS & Linux.
② Customize the app identifier string for your own applications.
33
Tkinter
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.bind("<Button-1>", handle_button_press)
button.pack()
window.mainloop()
① The code is wrapped in a try/except block since the windll module is not
available on macOS & Linux.
② Customize the app identifier string for your own applications.
34
wxPython
35
Listing 17. wxpython/custom/app_windows_taskbar.py
import os
import wx
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
class MainWindow(wx.Frame):
def __init__(self, parent, title):
wx.Frame.__init__(self, parent, title=title, size=(200, -1))
self.sizer = wx.BoxSizer(wx.VERTICAL)
self.sizer.Add(self.button)
self.SetSizer(self.sizer)
self.SetAutoLayout(True)
self.Show()
app = wx.App(False)
w = MainWindow(None, "Hello World")
w.SetIcon(wx.Icon(os.path.join(basedir, "icon.ico")))
app.MainLoop()
36
① The code is wrapped in a try/except block since the windll module is not
available on macOS & Linux.
② Customize the app identifier string for your own applications.
37
PySimpleGUI
import base64
import os
import PySimpleGUI as sg
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
def icon(filename): ③
path = os.path.join(basedir, filename)
with open(path, "rb") as f:
return base64.b64encode(f.read())
layout = [
[
sg.Button(
"My simple app.",
image_data=icon("icon.png"),
)
],
]
window = sg.Window(
"Hello World", layout, icon=os.path.join(basedir, "icon.ico")
)
while True:
event, values = window.read()
print(event, values)
38
if event == sg.WIN_CLOSED or event == "My simple app.":
break
window.close()
① The code is wrapped in a try/except block since the windll module is not
available on macOS & Linux.
② Customize the app identifier string for your own applications.
③ PySimpleGUI doesn’t have a native method for loading images. The docs
suggest base64 encoding them into the source, which is a bit
inconvenient — you’ll need to redo this on any changes. Instead you can
use this function like this to handle the loading and encoding for you at
runtime.
39
Kivy
import os
basedir = os.path.dirname(__file__)
Window.size = (300, 200)
try: ①
from ctypes import windll # Only exists on Windows.
myappid = "mycompany.myproduct.subproduct.version" ②
windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid)
except ImportError:
pass
class MainWindow(BoxLayout):
def __init__(self):
super().__init__()
button = ImageButton(
source=os.path.join(basedir, "icon.png"),
size=(32, 32),
pos=(16, 100 - 16),
)
button.bind(on_press=self.handle_button_clicked)
self.add_widget(button)
40
class MyApp(App):
def build(self):
self.title = "Hello World"
self.icon = os.path.join(basedir, "icon.png")
return MainWindow()
app = MyApp()
app.run()
① The code is wrapped in a try/except block since the windll module is not
available on macOS & Linux.
② Customize the app identifier string for your own applications.
41
The listing above shows the generic string
mycompany.myproduct.subproduct.version 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 a dotted hierarchical notation.
With this added to your script, the icon should now be visible on your
taskbar.
42
Listing 20. common/custom/hello-world-icons.spec
exe = EXE(pyz,
a.scripts,
[],
exclude_binaries=True,
name='hello-world',
icon='icon.ico',
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 )
To create an .ico file, I recommend you use Greenfish Icon Editor Pro, a
free and open-source tool which can also build icons for Windows. An
example .ico file is included in the downloads with this book.
If you run the pyinstaller build with the modified .spec file, you’ll see the
executable now has the custom icon.
43
macOS .app bundle icon (macOS only)
On macOS applications are distributed in .app bundles, which can have
their own icons. The bundle icon is used to identify the application in the
Launchpad and on the dock when the application is launched. PyInstaller
can take care of adding the icon to the app bundle for you, you just need to
pass an ICNS format file to the BUNDLE block in the .spec file. This icon will
then show up on the resulting bundle, and be shown when the app is
started.
app = BUNDLE(coll,
name='Hello World.app',
icon='icon.icns',
bundle_identifier=None)
ICNS is the file format for icon files on macOS. You can create icon files on
macOS using Icon Composer. You can also create macOS icons on Windows
using Greenfish Icon Editor Pro.
Figure 8. macOS .app bundle showing the default and custom icons.
In our example the icon set on the bundle will be replaced by the set icon
method calls in the code. However, on macOS you can skip setting the icon
44
from your application entirely and just set the icon through the .app bundle
if you wish.
Linux icons
On Linux there aren’t these additional hoops to jump though. Things will
mostly just work by setting the icon in your application — dependent on
which GUI library you’re using.
45
6. Relative paths
So far we’ve created a simple application and modified it to add a
window icon — nice and straightforward, right? But there is a gotcha here,
which might not be immediately apparent.
Open a shell and change to the folder where your script is saved. Run it as
normal:
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).
cd ..
python3 <folder>/app.py
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, but the
folder you ran it from. If you run the script from elsewhere it won’t be able
to find the files.
46
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 it’s data files. We need to fix this
before we go any further, which we can do by making our paths relative to
our application folder.
It is not immediately obvious, but when you provide just a filename for a
file, e.g. hello.jpg, that is a relative path. When the file is loaded, it is
loaded relative to the current active folder. Confusingly, the current active
folder is not necessarily the same folder your script is in.
Why not just use absolute paths? Because they will only work
on your own filesystem, or a filesystem with exactly the
same structure. If I develop an application in my own home
1. a way to find the path to the folder our currently running script is in
47
2. to take this path and use it to construct paths of the images we’re loading
The path of our current script is always available in Python with the global
variable __file__. We can get the folder of the file using os.path.dirname
— for directory name — which will give us the relative path to our project
folder from the current working directory.
Since our app.py file is in the root of our folder, all other paths are relative
to that.
48
PyQt6
import os
import sys
basedir = os.path.dirname(__file__)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Hello World")
self.setCentralWidget(button)
self.show()
app = QApplication(sys.argv)
app.setWindowIcon(QIcon(os.path.join(basedir, "icon.svg")))
w = MainWindow()
app.exec()
49
Tkinter
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()
window.mainloop()
50
wxPython
import os
import wx
basedir = os.path.dirname(__file__)
class MainWindow(wx.Frame):
def __init__(self, parent, title):
wx.Frame.__init__(self, parent, title=title, size=(200, -1))
self.sizer = wx.BoxSizer(wx.VERTICAL)
self.sizer.Add(self.button)
self.SetSizer(self.sizer)
self.SetAutoLayout(True)
self.Show()
app = wx.App(False)
w = MainWindow(None, "Hello World")
w.SetIcon(wx.Icon(os.path.join(basedir, "icon.ico")))
app.MainLoop()
51
PySimpleGUI
import base64
import os
import PySimpleGUI as sg
basedir = os.path.dirname(__file__)
def icon(filename):
path = os.path.join(basedir, filename)
with open(path, "rb") as f:
return base64.b64encode(f.read())
layout = [
[
sg.Button(
"My simple app.",
image_data=icon("icon.png"),
)
],
]
window = sg.Window(
"Hello World", layout, icon=os.path.join(basedir, "icon.ico")
)
while True:
event, values = window.read()
print(event, values)
if event == sg.WIN_CLOSED or event == "My simple app.":
break
window.close()
52
Kivy
53
Listing 26. kivy/custom/app_relative_paths.py
import os
basedir = os.path.dirname(__file__)
Window.size = (300, 200)
class MainWindow(BoxLayout):
def __init__(self):
super().__init__()
button = ImageButton(
source=os.path.join(basedir, "icon.png"),
size=(32, 32),
pos=(16, 100 - 16),
)
button.bind(on_press=self.handle_button_clicked)
self.add_widget(button)
class MyApp(App):
def build(self):
self.title = "Hello World"
self.icon = os.path.join(basedir, "icon.png")
return MainWindow()
app = MyApp()
app.run()
54
Try and run your app again — both from the same folder and the parent
folder — you’ll find that the icon now appears as expected, no matter where
you launch the app from.
55
7. Data files and Resources
So we now have a application working, with a custom name, custom
application icon and a couple of tweaks to ensure that the icon is displayed
on all platforms and wherever the application is launched from. With this
in place, 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.
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. This approach is not just for icon files, it
can be used for any other data files that need to be packaged with your
application.
56
PyQt6
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
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.pressed.connect(self.close)
layout.addWidget(button)
container = QWidget()
container.setLayout(layout)
57
self.setCentralWidget(container)
self.show()
app = QApplication(sys.argv)
app.setWindowIcon(QIcon(os.path.join(basedir, "icon.svg")))
w = MainWindow()
app.exec()
58
Tkinter
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()
59
wxPython
60
Listing 29. wxpython/data-file/app.py
import os
import wx
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
class MainWindow(wx.Frame):
def __init__(self, parent, title):
wx.Frame.__init__(self, parent, title=title, size=(200, -1))
self.sizer = wx.BoxSizer(wx.VERTICAL)
self.sizer.Add(self.label)
self.sizer.Add(self.button)
self.SetSizer(self.sizer)
self.SetAutoLayout(True)
self.Show()
app = wx.App(False)
w = MainWindow(None, "Hello World")
w.SetIcon(wx.Icon(os.path.join(basedir, "icon.ico")))
app.MainLoop()
61
PySimpleGUI
import PySimpleGUI as sg
import os
import base64
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
def icon(filename):
path = os.path.join(basedir, "icons", filename)
with open(path, "rb") as f:
return base64.b64encode(f.read())
layout = [
[sg.Text("My simple app.")],
[
sg.Button(
"Push",
image_data=icon("icon.png"),
)
],
]
window = sg.Window(
"Hello World", layout, icon=os.path.join(basedir, "icon.ico")
)
while True:
event, values = window.read()
print(event, values)
62
if event == sg.WIN_CLOSED or event == "Push":
break
window.close()
63
Kivy
import os
basedir = os.path.dirname(__file__)
Window.size = (300, 200)
try:
from ctypes import windll # Only exists on Windows.
myappid = "mycompany.myproduct.subproduct.version"
windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid)
except ImportError:
pass
class MainWindow(BoxLayout):
def __init__(self):
super().__init__(orientation="vertical")
label = Label(text="My simple app.")
button = ImageButton(
source=os.path.join(basedir, "icon.png"),
size=(32, 32),
pos=(16, 50 - 16),
)
button.bind(on_press=self.handle_button_clicked)
64
self.add_widget(label)
self.add_widget(button)
class MyApp(App):
def build(self):
self.title = "Hello World"
self.icon = os.path.join(basedir, "icon.png")
return MainWindow()
app = MyApp()
app.run()
65
The simplest way to get this data file 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.
Or via the datas list in the Analysis section of the spec file, as a 2-tuple of
source and destination locations.
a = Analysis(['app.py'],
pathex=[],
binaries=[],
datas=[('icon.svg', '.')],
hiddenimports=[],
hookspath=[],
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False)
pyinstaller hello-world.spec
In both cases we are telling PyInstaller to copy the specified file icon.svg to
the location . which means the output folder dist. We could specify other
66
locations here if we wanted. If you run the build, you should see your .svg
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 as expected.
Figure 11. The icon showing on the window (Windows) and dock (macOS and Ubuntu)
67
8. 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. To demonstrate
bundling folders of data files, lets add a few more buttons to our app and
add icons to them. We can place these icons under a folder named icons.
68
PyQt6
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
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_close = QPushButton("Close")
button_close.setIcon(
QIcon(os.path.join(basedir, "icons", "lightning.svg"))
)
button_close.pressed.connect(self.close)
layout.addWidget(button_close)
69
button_maximize = QPushButton("Maximize")
button_maximize.setIcon(
QIcon(os.path.join(basedir, "icons", "uparrow.svg"))
)
button_maximize.pressed.connect(self.showMaximized)
layout.addWidget(button_maximize)
container = QWidget()
container.setLayout(layout)
self.setCentralWidget(container)
self.show()
app = QApplication(sys.argv)
app.setWindowIcon(QIcon(os.path.join(basedir, "icons", "icon.svg")))
w = MainWindow()
app.exec()
70
Tkinter
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_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(
71
text="Maximize",
image=button_maximimize_icon,
)
button_maximize.bind("<Button-1>", handle_button_press)
button_maximize.pack()
72
wxPython
import os
import wx
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
class MainWindow(wx.Frame):
def __init__(self, parent, title):
wx.Frame.__init__(self, parent, title=title, size=(200, -1))
image = wx.Bitmap(
os.path.join(basedir, "icons", "lightning.png")
)
self.button_close = wx.Button(self, label="Close")
self.button_close.SetBitmap(image)
self.Bind(
wx.EVT_BUTTON, self.handle_close_button, self.button_close
)
image = wx.Bitmap(
os.path.join(basedir, "icons", "uparrow.png")
)
self.button_maximize = wx.Button(self, label="Maximize")
self.button_maximize.SetBitmap(image)
self.Bind(
wx.EVT_BUTTON,
self.handle_maximize_button,
self.button_maximize,
73
)
self.sizer = wx.BoxSizer(wx.VERTICAL)
self.sizer.Add(self.label)
self.sizer.Add(self.button_close)
self.sizer.Add(self.button_maximize)
self.SetSizer(self.sizer)
self.SetAutoLayout(True)
self.Show()
app = wx.App(False)
w = MainWindow(None, "Hello World")
w.SetIcon(wx.Icon(os.path.join(basedir, "icons", "icon.ico")))
app.MainLoop()
74
PySimpleGUI
import PySimpleGUI as sg
import os
import base64
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
def icon(filename):
path = os.path.join(basedir, "icons", filename)
with open(path, "rb") as f:
return base64.b64encode(f.read())
layout = [
[sg.Text("My simple app.")],
[
sg.Button(
"Close",
image_data=icon("lightning.png"),
)
],
[
sg.Button(
"Maximize",
image_data=icon("uparrow.png"),
)
],
]
window = sg.Window(
75
"Hello World",
layout,
icon=os.path.join(basedir, "icons", "icon.ico"),
)
while True:
event, values = window.read()
print(event, values)
if event == sg.WIN_CLOSED or event == "Close":
break
if event == "Maximize":
window.maximize()
window.close()
76
Kivy
import os
basedir = os.path.dirname(__file__)
Window.size = (300, 200)
try:
from ctypes import windll # Only exists on Windows.
myappid = "mycompany.myproduct.subproduct.version"
windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid)
except ImportError:
pass
class MainWindow(BoxLayout):
def __init__(self):
super().__init__(orientation="vertical")
label = Label(text="My simple app.")
button_close = ImageButton(
source=os.path.join(basedir, "icons", "lightning.png"),
)
button_close.bind(on_press=self.handle_close_button_clicked)
button_maximize = ImageButton(
source=os.path.join(basedir, "icons", "uparrow.png"),
)
77
button_maximize.bind(
on_press=self.handle_maximize_button_clicked
)
self.add_widget(label)
self.add_widget(button_close)
self.add_widget(button_maximize)
class MyApp(App):
def build(self):
self.title = "Hello World"
self.icon = os.path.join(basedir, "icons", "icon.png")
return MainWindow()
app = MyApp()
app.run()
78
The Windows taskbar icon fix is included in this code, you
The icons (both SVG files) are stored under a subfolder named 'icons'.
.
├── app.py
└── icons
└── lightning.svg
└── uparrow.svg
└── icon.svg
If you run this you’ll see the following window, with icons on the buttons
and an icon in the window or dock.
Figure 12. Windows with multiple icons running in PyQt6, Tkinter, wxPythn, Kivy &
PySimpleGUI.
79
We’ve updated the taskbar & dock icon paths, and they continue to work
across all platforms.
Figure 13. Icons & taskbar icons on Windows, macOS & Linux
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.
block_cipher = None
a = Analysis(['app.py'],
pathex=[],
binaries=[],
datas=[('icons', 'icons')],
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,
80
a.scripts,
[],
exclude_binaries=True,
name='hello-world',
icon='icons/icon.ico',
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')
app = BUNDLE(coll,
name='Hello World.app',
icon='icons/icon.icns',
bundle_identifier=None)
If you run the build using this spec file you’ll now see the icons folder
copied across to the dist folder. If you run the application from the folder
— or anywhere else — the icons will display as expected, as the relative
paths remain correct in the new location.
81
9. Hidden Imports, Excludes & Binaries
When you package something with PyInstaller it looks through your source
code to find all the imports it uses. It then looks through the source code of
those packages and looks for all the imports they use. This continues until
it’s traversed the entire package tree and found everything that should be
needed to run your application.
There are also cases where the reverse happens — PyInstaller finds and
imports something which really shouldn’t be included in your package. This is
usually less of a problem — unless it breaks the build, then your application
bundle will just end up a bit bigger. But you should still try and solve this if
you can.
To deal with both these situations PyInstaller provides a way to manually tell
it about imports it should include, or should not include in the package
you’re building. Like all PyInstaller configuration, these can be provided
either as command-line arguments, or by editing the .spec file.
Hidden imports
The hidden imports directive allows you to specify packages/modules to
include in your package build. These modules will be added to the list of
modules to include in the package. You can use this to manually add
modules which PyInstaller has failed to find while scanning your project —
for example conditional imports, or packages imported in a non-standard
way.
82
Or, editing the spec file.
a = Analysis(
['app.py'],
...
hiddenimports=['my_module'],
...
)
You can add as many hidden imports as you need for your project, and you
can also use wildcard imports to import submodules. For example, the
following is valid:
a = Analysis(
['app.py'],
...
hiddenimports=['my_module.*'],
...
)
import my_module.*
Excludes
The excludes directive allows you to specify packages/modules to exclude
from your package build. These modules will be stripped from the list of
modules to include in the package after PyInstaller has finished collecting
them. One place where excludes can help is when packaging a 3rd party
module which is trying to import something your application doesn’t need.
83
Matplotlib) by default. The build will fail with Failed to import module
__PyInstaller_hooks_0_pytest. It’s trying to packaging a testing
framework! I don’t need that in a distributed app.
There is already a hook for numpy in PyInstaller so the build should work out
of the box. While this will probably be fixed in a later version of the
PyInstaller hooks, that’s no good for me right now.
To stop the build failing I can add a simple exclude for PyTest with
a = Analysis(
['app.py'],
...
excludes=['pytest'],
...
)
Note if you need to exclude multiple modules you can provide the
--exclude-module option multiple times, or add multiple items to the
excludes=[].
Binaries
Some Python code depends on non-Python binary libraries — usually .dll
or .so files. These cannot be found by PyInstaller since they aren’t imported.
In that case you will need to manually add them to your build using the
binaries directive.
84
Binary files to include are specified with the complete path, together with a
destination location in the build package — just like data files.
a = Analysis(
['app.py'],
...
binaries=[( '/usr/lib/libiodbc.2.dylib', '.' )],
...
)
Your application (or the packaged library) will need to take care of loading
the library from the new location in the packaged application.
85
Building Installers
So far we’ve used PyInstaller to create a packaged executable version of our
applications, together with their libraries and data files. While you can
share the resulting folder with other users — for example as a .zip file —
and they will be able to run it on their computer, it’s not a very friendly way
to share your software.
Each platform has its own standards for distributing software and to give
the best user experience you should try and follow these. On Windows this
is usually executable setup installers, on macOS .app bundles in a Disk
Image and on Linux a distribution-specific package, such as a .deb.
Despite it’s name PyInstaller doesn’t create installers. So for the next step
we’re going to need some other tools. In this chapter we’ll walk step by step
through building platform-appropriate installers for Windows, macOS &
Linux.
86
10. Creating a Windows installer with
InstallForge
So far we’ve used PyInstaller to bundle applications for distribution. 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.
We’ll now walk through the basic steps of creating an installer with
InstallForge.
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.
87
Figure 14. InstallForge initial view, showing General settings.
You can also select the target platforms for the installer, from various
versions of Windows that are currently available. This ensures people can
only install your application on versions of Windows which are compatible
with it.
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.
88
Use "Add Files…" on the toolbar and select all the files in the dist/hello-
world 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/hello-world such as your icons folder and other libraries.
Figure 15. 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/hello-world to be present. But the folder dist/hello-world itself
should not be listed.
89
The default install path can be left as-is. The values between angled
brackets, e.g. <company> , are variables and will be filled automatically from
the configuration.
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 Windows "Add or Remove
Programs".
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
90
happens once the installer is complete, and it’s helpful here to give the user
the option to run your program once it’s installed.
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 hello-world.exe. Arguments can be used to pass any arguments
to the program on the first launch.
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.
91
Figure 18. 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 hello-world.exe
92
Figure 19. InstallForge, adding a Shortcut.
Build
With the basic settings in place, you can now build your installer.
Click on the "Build" section at the bottom to open the build panel.
93
Figure 20. InstallForge, ready to build.
Click on the Build icon on the toolbar 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.
The build process will began, collecting and compressing the files into the
installer.
94
Figure 21. 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.
95
Figure 22. 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.
96
Figure 23. Hello World in the Start Menu on Windows 11.
Wrapping up
In a previous chapter we covered how to build your Python applications
into a distributable executable using PyInstaller. In this chapter we’ve taken
this built PyInstaller application and walked through the steps of using
InstallForge to build an installer for the app. Following these steps you
should be able to package up your own applications and make them
available to other people on Windows.
97
Another popular tool for building Windows installers is
NSIS which is a scriptable installer, meaning you configure
98
11. Creating a macOS Disk Image Installer
In a previous chapter we used PyInstaller to build a macOS .app file from
our application. Opening this .app will run your application, and you can
technically distribute it to other people as it is. However, there’s a catch —
macOS .app files are actually just folders with a special extension. This
means they aren’t suited for sharing as they are — end users would need to
download all the individual files inside the folder.
The solution is to distribute the .app inside a Zip .zip or disk image .dmg file.
Most commercial software uses disk images since you can also include a
shortcut to the user’s Applications folder, allowing them to drag the
application over in a single move. This is now so common than many users
would be quite confused to be faced with anything else. Let’s just stick with
the convention.
create-dmg
It’s relatively straightforward to create DMG files yourself, but I’d
recommend starting by using the tool create-dmg which can be installed
from Homebrew. This tool installs as a simple command-line tool, which
you can call passing in a few parameters to generate your DMG installer.
Once installed you have access to the create-dmg bash script. Below is a
subset of the options, which can be displayed by running create-dmg
--help
99
--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,
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
--eula <eula_file>: attach a license file to the dmg
--no-internet-enable: disable automatic mount©
--format: specify the final image format (default is UDZO)
--add-file <target_name> <file|folder> <x> <y>: add additional file or
folder (can be used multiple times)
--disk-image-size <x>: set the disk image size manually to x MB
--version: show tool version number
-h, --help: display the help
Together with the options given above, you need to specify the output name
for your DMG file and an input folder — the folder containing your .app
generated by PyInstaller.
Below we’ll use create-dmg to create an installer DMG for our Hello World
application. We’re only using some of the available options here — setting
the name & icon of the disk volume, positioning and sizing the window,
setting the icon for our app and adding the /Applications drop destination
link. This is the bare minimum you will likely want to set for your own
applications, and you can customize it further yourself if you prefer.
Since create-dmg copies all files in the specified folder into the DMG you’ll
need to ensure that your .app file is in a folder by itself. I recommend
100
creating a folder dmg and copying the built .app bundle into it into it. Below
I’ve created a small script to perform the packaging, including a test to
check for and remove any previously-built DMG files.
#!/bin/sh
test -f "Hello World.dmg" && rm "Hello World.dmg"
test -d "dist/dmg" && rm -rf "dist/dmg"
# Make the dmg folder & copy our .app bundle in.
mkdir -p "dist/dmg"
cp -r "dist/Hello World.app" "dist/dmg"
# Create the dmg.
create-dmg \
--volname "Hello World" \
--volicon "icons/icon.icns" \
--window-pos 200 120 \
--window-size 800 400 \
--icon-size 100 \
--icon "Hello World.app" 200 190 \
--hide-extension "Hello World.app" \
--app-drop-link 600 185 \
"Hello World.dmg" \
"dist/dmg/"
Save this into the root of your project named build-dmg.sh and then make it
executable with.
$ chmod +x build-dmg.sh
$ ./build-dmg.sh
The create-dmg process will run and a DMG file will be created in the
current folder, matching the name you’ve given for the output file (the
second to last argument, with the .dmg extension). You can now distribute
101
the resulting DMG file to other macOS users!
Figure 24. The resulting Disk Image showing our .app bundle and the Applications
shortcut. Drag the app across to install.
102
12. Creating a Linux Package with fpm
In an previous chapter we 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 which can be shared with other users.
However, in order to make it easy for them to install it on their system, we
need to create a Linux package.
103
If you’re impatient, you can download the Example Ubuntu
Package first.
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.
Once ruby is installed, you can install fpm using the gem tool.
Once the installation is complete, you’re ready to use fpm. You can check it
is installed and working by running:
$ fpm --version
1.14.2
104
back and double check everything.
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.
$ mkdir -p package/opt
$ mkdir -p package/usr/share/applications
$ mkdir -p package/usr/share/icons/hicolor/scalable/apps
105
$ cp -r dist/hello-world package/opt/hello-world
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.
$ 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 a Scalable Vector Graphics (SVG) file so our icon belongs under
the scalable folder. 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.
$ cp icons/penguin.svg
package/usr/share/icons/hicolor/scalable/apps/hello-world.svg
106
Name the destination filename of the icon after your
[Desktop Entry]
# The type of the thing this desktop file refers to (e.g. can be Link)
Type=Application
# The icon for the entry, use the target filesystem path.
Icon=hello-world
Now the hello-world.desktop file is ready, we can copy it into our install
package with.
107
$ 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.
• -C the folder to change to before searching for files: our package folder
108
• -n the name of the application: "hello-world"
After a few seconds, you should see a message to indicate that the package
has been created.
Installation
The package is ready! Let’s install it.
Once installation has completed, you can check the files are where you
expect, under /opt/hello-world
109
$ 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).
Figure 26. Application shows up in the Ubuntu search panel, and will also appear in
menus on other environments.
110
Figure 27. Application runs and all icons show up as expected.
To avoid problems, I recommend scripting this with a simple bash script &
fpm’s own automation tool.
package.sh
Save in your project root and chmod +x to make it executable.
111
Listing 40. common/installer/linux/package.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
-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.
112
Executing the build
pyinstaller hello-world.spec
./package.sh
fpm
Feel free to customize these build scripts further yourself to suit your own
project!
In this chapter we’ve stepped through the process of taking a working build
from PyInstaller and using fpm to bundle this up into a distributable Linux
package for Ubuntu. Following these steps you should be able to package
up your own applications and make them available to other people.
113
Signing Executables
Modern operating systems make a concerted effort to stop users from
running software which can harm the system. Software downloaded from
unknown sources can pose a risk.
To mitigate this risk Windows and macOS will show quite deliberately
frightening messages when you try and launch unverified software. In both
cases verified means specifically that the software has been signed by a
known entity. If you don’t sign your software, your users may be scared off
from running it.
114
13. Signing Windows Applications
Unsigned applications on Windows (including installers) will show an
"Unknown Publisher" warning when run. While installation isn’t
necessarily blocked outright, it’s not a reassuring experience for your users.
There are a huge number of providers available, who all all offer basically
the same service at wildly different prices. Your best bet is to just try and
find the cheapest authentic provider you can.
115
machines (that report their installs). If that sounds like too high a bar for
your software, you may want to consider EV.
These links redirect to the 3rd Party sellers I’ve found with
the cheapest code signing certificates. They will include
Once you’ve purchased your certificate you can download it and use it —
you need the .pfx file for actually signing your executable.
Signing Executables
To sign binary files you can use the signtool command. The signtool can
be downloaded as part of the Windows 10 SDK.
Once installed, you can use the signtool tool as follows, passing in the
certificate you downloaded, your password and the binary file to be signed.
The /d parameter gives a description of what is being signed — for example,
your application name. The /t parameter is the URL to time server for time-
stamping the signed application. Finally /v enables verbose output.
116
Repeat this for every binary which needs to be signed in your application —
usually the main executable is enough. Python and other libraries have
already been signed by their respective creators, you don’t need to sign
them again.
Once the file(s) are signed, you can proceed to build the installer as normal.
Test the signed installer on your own computer to confirm that the
warnings have disappeared — if the signing process has completed
properly, there won’t be.
117
14. Signing macOS Application Bundles
Signing macOS application bundles is a pretty long-winded process, and
comes with a $99/year bill from Apple. But once you’ve done it your Python
applications will run without warnings on your user’s systems.
Entitlements .plist
First we need to create an entitlements.plist file which describes the
additional access or security exceptions that your application needs to run.
Because of the way that PyInstaller works there are a few standard entries
required. Below is an example working file.
Save this somewhere in your project, you’ll need to refer to in a later step.
Identity
Signing macOS software is done using an Apple ID. To sign software you
need to have an active developer account which currently costs $99/year.
Download and install X-Code from the Apple App Store. Open and then
navigate to XCode › Preferences in the menu and select the Accounts tab.
Click the [ + ] in the lower-left corner, choose Apple ID and enter your apple
118
ID and password. You can download and install existing keys from
https://developer.apple.com
Once your developer ID is installed, you can use it to sign the application. In
the terminal, enter the following command to get the hash ID of your
appropriate developer ID.
This will output a list of identities on your system. Take the 6-character
hash on the left as your identifier, and pass that to the codesign command
below. Replace MyApp.app with the name of your app bundle and use the
path where you saved the entitlements.plist in a previous step.
Once your .app bundle is signed to you can package it into a Disk Image as
normal.
Notarizing
Once you’ve completed the signing process above, the application will run,
but your users will still receive a warning the first time they run it.
To stop this happening you will need to submit your application to Apple
for notarization. See Apple’s notarization guide for the latest information.
119
Advanced Packaging
For most projects, the tips so far will be enough to get your application
packaged and successfully running on other computers. In this chapter
we’ll look at some additional tricks you can use to improve the portability
and usability of your application, as well as optimizing the build process to
get the best possible version of your packaged app.
120
15. Better Relative Paths
When we load external data files into our applications we typically do this
using paths. While straightforward in principle, there are a couple of ways
this can trip you up. As your applications grow in size, maintaining the
paths can get a bit unwieldly and it’s worth taking a step back to implement
a more reliable system.
In the Relative paths chatper we introduced a simple way to deal with this
problem when loading some image icons. There, we used the __file__
built-in to get the path of the currently running script (our application) and
then used os functions to first get the directory of our script and then use
that to build the full path.
import os
basedir = os.path.dirname(__file__)
# then ...
icon_path = os.path.join(basedir, "icon.svg")
This works well for simple applications where you have a single main script
and load relatively few files. But having to duplicate the basedir calculation
in every file you load from and use os.path.join to construct the paths
everywhere quickly turns into a maintenance headache. If you ever need to
restructure the files in your project, it’s not going to be fun. Thankfully
there is a simpler way!
121
To do this we can create a custom Paths class which uses a combination of
attributes and methods to build folder and file paths respectively. The core
of this is the same os.path.dirname(__file__) and os.path.join()
approach used above, with the added benefit of being self-contained and
easily modifiable.
Take the following code and add it to the root of your project, in a file
named paths.py.
import os
class Paths:
base = os.path.dirname(__file__)
images = os.path.join(base, "images")
icons = os.path.join(images, "icons")
data = os.path.join(base, "images")
# File loaders.
@classmethod
def icon(cls, filename):
return os.path.join(cls.icons, filename)
@classmethod
def image(cls, filename):
return os.path.join(cls.images, filename)
@classmethod
def data(cls, filename):
return os.path.join(cls.data, filename)
Now, anywhere in your application you can import the Paths class an use it
122
directly. The attributes base, icons, images, and data all return the paths to
their respective folders under the base folder. Notice how the icons folder
is constructed from the images path — nesting this folder under that one.
The methods icon, image and data are used to generate paths including
filenames. In each case you call the method passing in the filename to add
to the end of the path. These methods all depend on the folder attributes
described above. For example, if you want to load a specific icon you can
call the Paths.icon() method, passing in the name, to get the full path
back.
>>> Paths.icon('bug.png')
'U:\\home\\martin\\books\\create-simple-gui-applications\\code
\\further\\images\\icons\\bug.png'
In your application code you could use this as follows to construct the path
123
and load the icon.
QIcon(Paths.icon('bug.png'))
This keeps your code much tidier, helps ensure the paths are correct and
makes it much easier if you ever want to restructure how your files are
stored. For example, say you’ve moved the icons folder out of the images
folder and want to update the paths in your application: now you only need
to change the paths.py definition and all icons will work as before.
124
16. Detecting the current platform
Python GUI libraries do a good job of papering over the differences between
different platforms, allowing you to write code and be reasonably confident
it will work anywhere. However, there are times when it can be useful to
know the platform yourself — whether to get something working, or
improve the way something looks or behaves.
One simple example is changing the icon files you use on different
platforms. While you may be able to get things working using the same
files, using platform-specific types can offer benefits for icon scaling —
ensuring your app looks as good as possible.
Thankfully it’s easy to detect the current platform with Python using either
sys.platform
import sys
sys.platform
'win32'
Or platform.name
import platform
platform.name
'Windows'
The names used for the common platforms are shown below.
You can use these values to optionally run different code paths using if /
125
else blocks. If you want to load different data files (such as icons) on
different platforms you can combine this with the earlier [relative paths]
example, using the platform name to build alternate paths.
import os
import sys
class Paths:
base = os.path.dirname(__file__)
platform = os.path.join(
base, sys.platform
) # platform specific folders
images = os.path.join(base, "images")
icons = os.path.join(platform, "icons")
data = os.path.join(base, "images")
# File loaders.
@classmethod
def icon(cls, filename):
return os.path.join(cls.icons, filename)
@classmethod
def image(cls, filename):
return os.path.join(cls.images, filename)
@classmethod
def data(cls, filename):
return os.path.join(cls.data, filename)
In the above example, the application will load the icons from
<base>/win32/icons or <base>/darwin/icons depending on which platform
it is run on.
126
17. Working with command-line
arguments
If you have created an application which works with specific file
types — for example a video editor that opens videos, a document editor
that opens document files — it can be useful to have your application open
these files automatically. On all platforms, when you tell the OS to open a
file with a specific application, the filename to open is passed to that
application as a command-line argument.
When your application is run, the arguments passed to the application are
always available in sys.argv. To open files automatically, you can check
the value of sys.argv at startup and, if you find a filename in there, open it.
The following app when run will open a window with all the command line
arguments received displayed.
127
PyQt6
import sys
class Window(QWidget):
def __init__(self):
super().__init__()
layout = QVBoxLayout()
self.setLayout(layout)
self.setWindowTitle("Arguments")
app = QApplication(sys.argv)
w = Window()
w.show()
app.exec()
128
Tkinter
import tkinter as tk
import sys
window = tk.Tk()
window.title("Arguments")
129
wxPython
import wx
import sys
class MainWindow(wx.Frame):
def __init__(self, parent, title):
wx.Frame.__init__(self, parent, title=title, size=(200, -1))
self.sizer = wx.BoxSizer(wx.VERTICAL)
self.SetSizer(self.sizer)
self.SetAutoLayout(True)
self.Show()
app = wx.App(False)
w = MainWindow(None, "Arguments")
app.MainLoop()
130
PySimpleGUI
import PySimpleGUI as sg
import sys
layout = []
for arg in sys.argv: ①
layout.append(
[sg.Text(arg)],
)
while True:
event, values = window.read()
print(event, values)
if event == sg.WIN_CLOSED:
break
window.close()
131
Kivy
import sys
class Window(QWidget):
def __init__(self):
super().__init__()
layout = QVBoxLayout()
self.setLayout(layout)
self.setWindowTitle("Arguments")
app = QApplication(sys.argv)
w = Window()
w.show()
app.exec()
132
Run this app from the command line, passing in a filename (you can make
anything up, we don’t load it). You can pass as many, or as few, arguments
as you like.
This will produce the window below. Notice that when run with python the
first argument is actually the Python file which is being executed.
Figure 30. The window open showing the command line arguments.
If you package your application for distribution, this may no longer be the
case — the first argument may now be the file you are opening, as there is
no Python file passed as an argument. This can cause problems, but a
simple way around this is to use the last argument passed to your
application as the filename, e.g.
if len(sys.argv) > 0:
filename_to_open = sys.argv[-1]
133
if __file__ in sys.argv:
sys.argv.remove(__file__)
134
PyQt6
import sys
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.editor = QTextEdit()
if __file__ in sys.argv: ①
sys.argv.remove(__file__)
if sys.argv: ②
filename = sys.argv[0] ③
self.open_file(filename)
self.setCentralWidget(self.editor)
self.setWindowTitle("Text viewer")
self.editor.setPlainText(text)
app = QApplication(sys.argv)
w = MainWindow()
w.show()
app.exec()
135
③ Take the first argument as the filename to open.
136
Tkinter
import tkinter as tk
import sys
window = tk.Tk()
window.title("Text viewer")
if __file__ in sys.argv: ①
sys.argv.remove(__file__)
def open_file(filename):
with open(filename, "r") as f:
return f.read()
text = ""
if sys.argv: ②
filename = sys.argv[0] ③
text = open_file(filename)
textbox = tk.Text()
textbox.insert(tk.END, text)
textbox.pack()
137
wxPython
import wx
import sys
class MainWindow(wx.Frame):
def __init__(self, parent, title):
wx.Frame.__init__(self, parent, title=title, size=(400, 300))
self.sizer = wx.BoxSizer(wx.VERTICAL)
self.text = wx.TextCtrl(self, style=wx.TE_MULTILINE)
self.sizer.Add(self.text, wx.EXPAND, wx.EXPAND)
if __file__ in sys.argv: ①
sys.argv.remove(__file__)
if sys.argv: ②
filename = sys.argv[0] ③
self.text.LoadFile(filename)
self.SetSizer(self.sizer)
self.SetAutoLayout(True)
self.Show()
app = wx.App(False)
w = MainWindow(None, "Text viewer")
app.MainLoop()
138
PySimpleGUI
import PySimpleGUI as sg
import sys
if __file__ in sys.argv: ①
sys.argv.remove(__file__)
text = ""
if sys.argv: ②
filename = sys.argv[0] ③
with open(filename, "r") as f:
text = f.read()
layout = [
[sg.Text(text)],
]
while True:
event, values = window.read()
print(event, values)
if event == sg.WIN_CLOSED:
break
window.close()
139
Kivy
import sys
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.editor = QTextEdit()
if __file__ in sys.argv: ①
sys.argv.remove(__file__)
if sys.argv: ②
filename = sys.argv[0] ③
self.open_file(filename)
self.setCentralWidget(self.editor)
self.setWindowTitle("Text viewer")
self.editor.setPlainText(text)
app = QApplication(sys.argv)
w = MainWindow()
w.show()
app.exec()
140
③ Take the first argument as the filename to open.
141
You can run this as follows, to view the passed in text file.
142
18. Optimizing Packages
If you’re using 3rd party modules your excitement at successfully packaging
your application may be short lived. Once you try and share it with
someone else you realize it’s absolutely enormous.
While access to fast internet makes this less of a problem than it used to be,
you should always try and keep your application installer as small as
possible to make it easy & quick to download or share.
Optional imports
Some packages have optional support allowing them to work with other
packages. Typically they import these other packages within a try/except
block, meaning that if the other package doesn’t exist, everything will
continue to work.
If your application doesn’t need these 3rd party optional packages, you don’t
want them included in your package.
143
The best solution to this is to use virtual environments to ensure that you
have a clean Python environment containing only the packages that your
application needs to run. If you’re unable to use virtual environments, or
the package your using has optional packages in it’s requirements (so
they’re getting installed automatically) you can instead use PyInstaller
excludes directives to remove them.
Tkinter
To avoid this happening you will need to add a specific exclude for Tkinter:
a = Analysis(
['app.py'],
...
excludes=['tkinter'],
...
)
Matplotlib
144
Matplotlib has optional imports for a number of different UI frameworks. If
you have these installed, they’ll be pulled into the package automatically.
If your system also has PyQt6, PySide6 and PySide2 installed, you could
exclude them with.
excludes=['PyQt6','PySide6','PySide2'],
Pandas
Pandas has quite a long list of optional dependencies which you can
add/remove depending on what you’re doing.
excludes=['matplotlib'],
Or, if you are not loading/saving Excel files, you could also choose to
exclude:
Or, if you’re not using pandas for linear algebra, you could exclude:
excludes=['scipy'],
145
Tests
Some packages bundle their own tests, which are never required in your
packaged applications. Look for any folders named tests in the packaged
dist folder and exclude them from PyInstaller using excludes in the .spec
file. For example —
excludes=['tests'],
In that case excluding the packages using excludes won’t work, and you
instead need to modify PyInstaller file lists directly.
a = Analysis(['app.py'],
binaries=[])
These file lists are then passed to COLLECT to be passed into the output
folder. Notice that COLLECT is passed a.binaries and a.datas, etc. — these
are the results of the analysis.
146
coll = COLLECT(
exe,
a.binaries,
a.zipfiles,
a.datas,
strip=False,
upx=True,
upx_exclude=[],
name='app'
)
The a.binaries (and other) file lists are of type TOC, which is a custom set
type. This means you can modify the files in there by creating your own TOC
and subtracting it from the list. For example, you could use the following to
remove Tkinter binaries from a build.
The code would be added after the ANALYSIS block and before the COLLECT
block.
If you run a build with this modification, the listed files will not be copied
into the dist folder during the packaging process.
147
19. PyInstaller Hooks
So far we’ve been packaging our applications using the built-in PyInstaller
hooks. As we’ve already learnt, hooks are the mechanism that PyInstaller
uses to understand how to package 3rd party packages. Hooks include
instructions telling PyInstaller what to include and exclude from the
resulting package, or additional data files that the package depends on,
which must also be copied.
If you can you should always try and use the built-in hooks first, if only
because figuring out the problems and writing the hooks isn’t much fun.
However, sometimes it’s unavoidable. For example, perhaps you’ve put
some of your own code into a 3rd party package for internal development
reasons and PyInstaller isn’t packaging it correctly.
You can fix these issues by manually adding files to the .spec file, but it can
turn into a bit of a mess. If you want to control packaging properly on a per-
module basis, you should write your own hook file.
Hooks work exactly the same as the .spec file, but allow you to structure
your additional imports, excludes and other files on a per-module basis.
148
— that is, the name starts with hook- followed by the complete dotted
Python import hierarchy. Often this is just the module name.
hiddenimports = [
"dns.rdtypes.*",
"dns.rdtypes.ANY.*"
]
You can use excludes to exclude imports from being bundled with your
application. Note however, that if this import is explictly imported in
another included module or source file, it won’t be removed.
excludes = ['tkinter']
The above will exclude Tkinter from being packaged unless it is explicitly
imported by another package.
You can also use hooks to specify data and binary files to include, just as you
would in the .spec file.
Using hooks
When you have written a custom hook, you need to tell PyInstaller about it.
The simplest way to do this is to save the hook in a folder in your project
149
folder — I suggest hooks — and then point PyInstaller at that folder.
a = Analysis(
['app.py'],
...
hookspath=['hooks'],
...
)
150
Troubleshooting
Of course it would be nice if every time you packaged your applications
it worked first time. With the recent improvement in PyInstaller hooks, the
chance of this being the case is much higher than it used to be. But still
things may go wrong.
Below we’ll cover some of the common issues you’ll see when trying to
package applications along with simple fixes to get you past that point.
151
20. Build doesn’t complete
Tried to package your application and got a strange error? You’re in the
right place.
The first step is to look at the error message — in some cases PyInstaller will
throw exceptions to halt the build for good reasons, such as your platform
not being supported or a later version of Python being required. If you see
one of those errors either upgrade your Python version, or upgrade
PyInstaller.
However, sometimes the errors don’t make much sense at all. In that case,
read on — I’ve added two common errors and their solutions below.
152
ModuleNotFound Errors
During the build process you will often see ModuleNotFound errors. These
occur when PyInstaller tries to import optional dependencies of modules &
those modules are not available in your Python environment. Usually these
can be ignored — if they’re not in your environment, it’s probably because
your application doesn’t need them.
However, in some cases these errors can cause the build to fail. In that case
something is wrong. Look at the module which is throwing errors — if it’s
something your application doesn’t need you can try to add the module to
the excludes, so PyInstaller will ignore it (see PyTest below).
If it’s something your application does need, then your first step should
always be to try and upgrade the package itself and/or the Pyinstaller hooks.
153
importlib_load_source
return mod_loader.load_module()
File "<frozen importlib._bootstrap_external>", line 407, in
_check_name_wrapper
File "<frozen importlib._bootstrap_external>", line 907, in
load_module
File "<frozen importlib._bootstrap_external>", line 732, in
load_module
File "<frozen importlib._bootstrap>", line 265, in _load_module_shim
File "<frozen importlib._bootstrap>", line 696, in _load
File "<frozen importlib._bootstrap>", line 677, in _load_unlocked
File "<frozen importlib._bootstrap_external>", line 728, in
exec_module
File "<frozen importlib._bootstrap>", line 219, in
_call_with_frames_removed
File "python37\lib\site-
packages\_pyinstaller_hooks_contrib\hooks\stdhooks\hook-pytest.py",
line 18, in <module>
import pytest
File "python37\lib\site-packages\pytest\__init__.py", line 6, in
<module>
from _pytest import __version__
ModuleNotFoundError: No module named '_pytest'
...
PyInstaller.exceptions.ImportErrorWhenRunningHook: Failed to import
module __PyInstaller_hooks_0_pytest
--excludes=pytest
excludes=['pytest'],
154
This same approach can be used to exclude any other
There are different options depending on your platform, but as a first step I
recommend using virtual environments to ensure the Python executable is in
a predictable location (and in the PATH).
If this doesn’t help, or you don’t want to do that you can also tell PyInstaller
the location of the Python library by using the LD_LIBRARY_PATH (Linux) or
DYLD_LIBRARY_PATH (macOS) environment variables.
If that still doesn’t work, the issue may be with your Python installation
itself. Try installing a new version of Python, creating a new virtual
environment and attempting to package the app with this.
155
21. Built application doesn’t run
Sometimes your PyInstaller build will complete, but the resulting
application won’t start. Usually these failures are silent, with the
application either just not starting or flashing a window for a fraction of a
second. Not helpful!
Windows
Your first step on Windows should be to re-enable the console in your .spec
file. This will allow you to see the debug output from the running
application — it’s much easier to debug if you have something to go on!
exe = EXE(pyz,
a.scripts,
[],
...
console=True, # Enable the console, so you can see debug
output.
...
)
macOS
On macOS you can run the application from the command line as-is,
without modifying the .spec file — the debug output will be shown there for
you. Open a terminal and navigate to your .app bundle, then cd inside.
156
cd MyApp.app
./myapp
Linux
On Linux you can run the executable from the terminal. There are no app
bundles, etc. in Linux, so just navigate to the folder containing the
executable and execute it as for any other executable or script.
./myapp
Common Errors
Once you have the error/logging output from running your app, you can
proceed to debugging it. See the next sections for some tips for debugging
common problems with PyInstaller builds.
ModuleNotFoundError
The most common error you’ll see when running your built PyInstaller
applications is Module not found. This error means that the application is
trying to import a package/module but not able to find it. In packaged
157
applications this is most likely caused by the module not being copied over
to the build folder.
Usually, this happens because PyInstaller has not seen the module imported
while analyzing your code (or one of the modules it uses). You can fix this
by explicitly adding the module to the hiddenimports directive.
--hidden-import=my_module
hiddenimports=['my_module'],
a = Analysis(
['app.py'],
...
hiddenimports=['my_module.*'],
...
)
import my_module.*
After adding the hidden imports retry the build and see if it works now. If
158
not, you’ll likely see another error about something else which is missing —
add this too! Carry on repeating this process until you’ve identified all the
missing imports.
159
Graphviz
This behavior has been patched in the latest PyInstaller hooks update. To
fix, simply update your hooks.
160
PyQtGraph
This error may be seen if using a mismatched version of PyQtGraph and the
PyInstaller hooks files.
no module pyqtgraph.graphicsItems.ViewBox.axisCtrlTemplate_pyqt5
If the problem persists, see the PyInstaller hooks Issues for latest updates.
161
yt_dlp
The library uses hidden imports which are not detected by PyInstaller.
While the module will package successfully the resulting executable will
not run.
You can fix this by informing PyInstaller about the hidden imports.
--hidden-import=yt_dlp.compat._legacy
hiddenimports=['yt_dlp.compat._legacy'],
162
Appendix A: What next?
This book covers the key things you need to know to start Packaging GUI
applications with Python. If you’ve made it here you should be well on your
way to create your own apps!
But there is still a lot to discover while you build your applications. To help
with this I post regular tips, tutorials and code snippets on the Python GUIs
website. Like this book all samples are MIT licensed and free to mix into
your own apps. You may also be interested in joining my Python GUI
Academy where I have video tutorials covering the topics in this book &
beyond!
Thanks for reading, and if you have any feedback or suggestions please let
me know!
163
Appendix B: Other Books by Me
If you’re interested in Python GUI programming you may be interested in
reading my other books, shown below. You can see the full list of my
Python GUI books on the Python GUIs website, or my other books on my
own website.
Figure 31. Create GUI Applications with Python & Qt6, 5th Edition PyQt6
164
Figure 32. Create GUI Applications with Python & Qt5, 5th Edition PyQt5
165
Figure 33. Create GUI Applications with Python & Qt6, 5th Edition PySide6
166
Index
A pytest, 153
app bundle, 44
R
arguments, 127
relative paths, 46
C
S
command-line arguments, 127
spec file, 18
D sys.argv, 127
data files, 56
T
data folders, 68
troubleshooting, 151
Debian, 103
U
E
Ubuntu, 103
executable icons, 42
V
F
virtual environment, 4
fpm, 103
Y
G
youtube, 162
Graphviz, 160
I
icons, 32
installer, 95
InstallForge, 87
L
Linux package, 103
P
paths, 46
PyInstaller, 103, 3, 87
pyqtgraph, 161
167