Take advantage of PyInstaller to package up your Python app into a standalone executable for easy distribution
To be fair, the original use case for Python never called for standalone packages. Python programs have, by and large, been run in-place on systems where a copy of the Python interpreter lived. But the surging popularity of Python has created greater demand for running Python apps on systems with no installed Python runtime.
Several third parties have engineered solutions for deploying standalone Python apps. The most popular solution of the bunch, and the most mature, is PyInstaller. PyInstaller doesn’t make the process of packaging a Python app to go totally painless, but it goes a long way there.
In this article we’ll explore the basics of using PyInstaller including how PyInstaller works, how to use PyInstaller to create a standalone Python executable, how to fine-tune the Python executables you create, and how to avoid some of the common pitfalls that go with using PyInstaller.
Table of Contents
- Creating a PyInstaller package
- Testing a PyInstaller package
- Refining a PyInstaller package
- PyInstaller tips
Creating a PyInstaller package
PyInstaller is a Python package, installed with
pip install pyinstaller). PyInstaller can be installed in your default Python installation, but it’s best to create a virtual environment for the project you want to package and install PyInstaller there.
PyInstaller works by reading your Python program, analyzing all of the imports it makes, and bundling copies of those imports with your program. PyInstaller reads in your program from its entry point. For instance, if your program’s entry point is
myapp.py, you would run
pyinstaller myapp.py to perform the analysis. PyInstaller can detect and automatically package many common Python packages, like NumPy, but you might need to provide hints in some cases. (More on this later.)
After analyzing your code and discovering all of the libraries and modules it uses, PyInstaller then generates a “spec file.” A Python script with the extension
.spec, this file includes details about how your Python app needs to be packed up. The first time you run PyInstaller on your app, PyInstaller will generate a spec file from scratch and populate it with some sane defaults. Don’t discard this file; it’s the key to refining a PyInstaller deployment!
Finally, PyInstaller attempts to produce an executable from the app, bundled with all its dependencies. When it’s finished, a subfolder named
dist (by default; you are free to specify a different name) will appear in the project directory. This in turn contains a directory that is your bundled app — it has an
.exe file to run, along with all of the libraries and other supplemental files required.
All you need to do to distribute your program, then, is package up this directory as a
.zip file or some other bundle. The bundle will typically need to be extracted in a directory where the user has write permissions in order to run.
Testing a PyInstaller package
There’s a fair chance your first attempt to use PyInstaller to package an app won’t be completely successful.
To check whether your PyInstaller package works, navigate to the directory containing the bundled executable and run the
.exe file there from the command line. If it fails to run, the errors you’ll see printed to the command line should provide a hint as to what’s wrong.
The most common reason a PyInstaller package fails is that PyInstaller failed to bundle a required file. Such missing files fall into a few categories:
- Hidden or missing imports: Sometimes PyInstaller can’t detect the import of a package or library, typically because it is imported dynamically. The package or library will need to be manually specified.
- Missing standalone files: If the program depends on external data files that need to be bundled with the program, PyInstaller has no way of knowing. You’ll need to manually include the files.
- Missing binaries: Here again, if your program depends on an external binary like a .DLL that PyInstaller can’t detect, you’ll need to manually include it.
The good news is that PyInstaller provides an easy way to deal with the above problems. The
.spec file created by PyInstaller includes fields we can fill in to provide the details that PyInstaller missed.
.spec file in a text editor and look for the definition of the
Analysis object. Several of the parameters passed to
Analysis are blank lists, but they can be edited to specify the missing details:
hiddenimportsfor hidden or missing imports: Add to this list one or more strings with the names of libraries you want included with your app. If you wanted to add
bokeh, for instance, you would specify that as
['pandas','bokeh']. Note that the libraries in question must be installed in the same instance of Python where you’re running PyInstaller.
datasfor missing standalone files: Add here one or more specifications for files in your project tree that you want to include with your project. Each file has to be passed as a tuple indicating the relative path to the file in your project directory and the relative path within the distribution directory where you want to place the file. For instance, if you had a file
./models/mainmodel.datthat you wanted to include with your app, and you want to place it in a matching subdirectory in your distribution directory, you would use
('./models/mainmodel.dat','./models')as one entry in the
hiddenimportslist. Note that you can use
glob-style wildcards to specify more than one file.
binariesfor missing standalone binaries: As with
datas, you can use
binariesto pass a list of tuples that specify the locations of binaries in the project tree and their destinations in the distribution directory. Again, you can use
Keep in mind that any of the lists passed to
Analysis can be programmatically generated earlier in the
.spec file. After all, the
.spec file is just a Python script by another name.
After you make changes to the
.spec file, rerun PyInstaller to rebuild the package. However, from now on, be sure to pass the modified
.spec file as the parameter (e.g.
pyinstaller myapp.spec). Test the executable as before. If something is still broken, you can re-edit the
.spec file and repeat the process until everything works.
Finally, when you’re satisfied everything works as intended, you might want to edit the
.spec file to prevent your packaged app from presenting a command-line window when launched. In the
EXE object settings in the
.spec file, set
console=False. Suppressing the console is useful if your app has a GUI and you don’t want a spurious command-line window leading users astray. Of course, don’t change this setting if your app requires a command line.
Refining a PyInstaller package
Once you have your app packaged with PyInstaller and running properly, the next thing you’ll likely want to do is slim it down a little. PyInstaller packages are not known for being svelte.
Because Python is a dynamic language, it’s difficult to predict just what will be needed at runtime by a given program. For that reason, when PyInstaller detects a package import, it includes everything in that package, whether or not it’s actually used at runtime by your program.
Here’s the good news. PyInstaller includes a mechanism for selectively excluding entire packages, or individual namespaces within packages. For instance, let’s say your program imports package
foo, which includes
foo.bip. If you know for a fact that your program only uses logic in
foo.bar, you can safely exclude
foo.bip and save some space.
To do this, you use the
excludes parameter passed to the
Analysis object in the
.spec file. You can pass a list of names — top-level modules, or dotted namespaces — to exclude from your package. For example, to exclude
foo.bip, you would simply specify
One common exclusion you can make is
tkinter, the Python library for creating simple cross-platform graphical user interfaces. By default,
tkinter and all of its support files are packed with a PyInstaller project. If you’re not using
tkinter in your project, you can exclude it by adding
'tkinter' to the
excludes list. Omitting
tkinter will reduce the size of the package by around 7 MB.
Another common exclusion is test suites. If a package your program imports has a test suite, the test suite could end up being included in your PyInstaller package. Unless you actually run the test suite in your deployed program, you can safely exclude it.
Bear in mind that packages created using exclusions should be thoroughly tested before being used. If you end up excluding functionality that is used in some future scenario you didn’t anticipate, your app will break.
- Build your PyInstaller package on the OS you plan to deploy on. PyInstaller does not support cross-platform builds. If you need to deploy your standalone Python app on MacOS, Linux, and Windows systems, then you will need to install PyInstaller and build separate versions of the app on each of these operating systems.
- Build your PyInstaller package as you develop your app. As soon as you know you will be deploying your project with PyInstaller, build your
.specfile and start refining the PyInstaller package in parallel with the development of your app. This way you can add exclusions or inclusions as you go, and test the way new features are deployed with the app as you write them.
- Don’t use PyInstaller’s
--onefilemode. PyInstaller includes a command line switch,
--onefile, that packs your entire app into a single self-extracting executable. This sounds like a great idea — you only have to deliver one file! — but it has some pitfalls. Whenever you run the app, it must first unpack all of the files within the executable to a temporary directory. If the app is big (200MB, for instance), unpacking can mean a delay of several seconds. Use the default single-directory mode instead, and just pack everything up as a
- Create an installer for your PyInstaller app. If you want some way to deploy your app other than a .zip file, consider using an installer utility like the open source Nullsoft Scriptable Install System. It adds very little overhead to the size of the deliverable and lets you configure many aspects of the installation process, like creating shortcuts to your executable.
- Don’t expect speedups. PyInstaller is a packaging system, not a compiler or an optimizer. Code packaged with PyInstaller does not run any faster than it would when run on the original system. If you want to speed up Python code, use a C-accelerated library suited to the task, or a project like Cython.