How to use PyInstaller to create Python executables

Take advantage of PyInstaller to package up your Python app into a standalone executable for easy distribution

How to use PyInstaller to create Python executables
masterzphotois / Getty Images

Python, powerful and versatile as it is, lacks a few key capabilities out of the box. For one, Python provides no native mechanism for compiling a Python program into a standalone executable package.

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.

Creating a PyInstaller package

PyInstaller is a Python package, installed with pip (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.

pyinstaller1 IDG

A .spec file is used to create an executable with PyInstaller. Note the namespaces listed in excludes. These will be left out of the created bundle to save space.

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.

Open the .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:

  • hiddenimports for 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 pandas and 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.
  • datas for 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.dat that 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 hiddenimports list. Note that you can use glob-style wildcards to specify more than one file.
  • binaries for missing standalone binaries: As with datas, you can use binaries to 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 glob-style wildcards.

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.bar and 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 ['foo.bip'].

pyinstaller2 IDG

The deliverable directory created by PyInstaller. Because of the large number of files in the directory, a third-party installer project like NSIS may be used to pack up the directory into a self-extracting archive, and automatically create a shortcut to the executable.

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.

PyInstaller tips

  • 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 .spec file 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 --onefile mode. 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 .zip file.
  • 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.

How to do more with Python

Copyright © 2020 IDG Communications, Inc.