Using PyInstaller to Easily Distribute Python Applications

Using PyInstaller to Easily Distribute Python Applications

by Luke Lee Updated Reading time estimate 21m intermediate best-practices

Are you jealous of Go developers building an executable and easily shipping it to users? Wouldn’t it be great if your users could run your application without installing anything? That is the dream, and PyInstaller is one way to get there in the Python ecosystem.

There are countless tutorials on how to set up virtual environments, manage dependencies, avoid dependency pitfalls, and publish to PyPI, which is useful when you’re creating Python libraries. There is much less information for developers building Python applications. This tutorial is for developers who want to distribute applications to users who may or may not be Python developers.

In this tutorial, you’ll learn the following:

  • How PyInstaller can simplify application distribution
  • How to use PyInstaller on your own projects
  • How to debug PyInstaller errors
  • What PyInstaller can’t do

PyInstaller gives you the ability to create a folder or executable that users can immediately run without any extra installation. To fully appreciate PyInstaller’s power, it’s useful to revisit some of the distribution problems PyInstaller helps you avoid.

Distribution Problems

Setting up a Python project can be frustrating, especially for non-developers. Often, the setup starts with opening a Terminal, which is a non-starter for a huge group of potential users. This roadblock stops users even before the installation guide delves into the complicated details of virtual environments, Python versions, and the myriad of potential dependencies.

Think about what you typically go through when setting up a new machine for Python development. It probably goes something like this:

  • Download and install a specific version of Python
  • Set up pip
  • Set up a virtual environment
  • Get a copy of your code
  • Install dependencies

Stop for a moment and consider if any of the above steps make any sense if you’re not a developer, let alone a Python developer. Probably not.

These problems explode if your user is lucky enough to get to the dependencies portion of the installation. This has gotten much better in the last few years with the prevalence of wheels, but some dependencies still require C/C++ or even FORTRAN compilers!

This barrier to entry is way too high if your goal is to make an application available to as many users as possible. As Raymond Hettinger often says in his excellent talks, “There has to be a better way.”

PyInstaller

PyInstaller abstracts these details from the user by finding all your dependencies and bundling them together. Your users won’t even know they’re running a Python project because the Python Interpreter itself is bundled into your application. Goodbye complicated installation instructions!

PyInstaller performs this amazing feat by introspecting your Python code, detecting your dependencies, and then packaging them into a suitable format depending on your Operating System.

There are lots of interesting details about PyInstaller, but for now you’ll learn the basics of how it works and how to use it. You can always refer to the excellent PyInstaller docs if you want more details.

In addition, PyInstaller can create executables for Windows, Linux, or macOS. This means Windows users will get a .exe, Linux users get a regular executable, and macOS users get a .app bundle. There are some caveats to this. See the limitations section for more information.

Preparing Your Project

PyInstaller requires your application to conform to some minimal structure, namely that you have a CLI script to start your application. Often, this means creating a small script outside of your Python package that simply imports your package and runs main().

The entry-point script is a Python script. You can technically do anything you want in the entry-point script, but you should avoid using explicit relative imports. You can still use relative imports throughout the rest your application if that’s your preferred style.

You can give this a try with your own project or follow along with the Real Python feed reader project. For more detailed information on the reader project, check out the the tutorial on Publishing a Package on PyPI.

The first step to building an executable version of this project is to add the entry-point script. Luckily, the feed reader project is well structured, so all you need is a short script outside the package to run it. For example, you can create a file called cli.py alongside the reader package with the following code:

Language: Python
from reader.__main__ import main

if __name__ == '__main__':
    main()

This cli.py script calls main() to start up the feed reader.

Creating this entry-point script is straightforward when you’re working on your own project because you’re familiar with the code. However, it’s not as easy to find the entry-point of another person’s code. In this case, you can start by looking at the setup.py file in the third-party project.

Look for a reference to the entry_points argument in the project’s setup.py. For example, here’s the reader project’s setup.py:

Language: Python
setup(
    name="realpython-reader",
    version="1.0.0",
    description="Read the latest Real Python tutorials",
    long_description=README,
    long_description_content_type="text/markdown",
    url="https://github.com/realpython/reader",
    author="Real Python",
    author_email="info@realpython.com",
    license="MIT",
    classifiers=[
        "License :: OSI Approved :: MIT License",
        "Programming Language :: Python",
        "Programming Language :: Python :: 2",
        "Programming Language :: Python :: 3",
    ],
    packages=["reader"],
    include_package_data=True,
    install_requires=[
        "feedparser", "html2text", "importlib_resources", "typing"
    ],
    entry_points={"console_scripts": ["realpython=reader.__main__:main"]},
)

As you can see, the entry-point cli.py script calls the same function mentioned in the entry_points argument.

After this change, the reader project directory should look like this, assuming you checked it out into a folder called reader:

reader/
|
├── reader/
|   ├── __init__.py
|   ├── __main__.py
|   ├── config.cfg
|   ├── feed.py
|   └── viewer.py
|
├── cli.py
├── LICENSE
├── MANIFEST.in
├── README.md
├── setup.py
└── tests

Notice there is no change to the reader code itself, just a new file called cli.py. This entry-point script is usually all that’s necessary to use your project with PyInstaller.

However, you’ll also want to look out for uses of __import__() or imports inside of functions. These are referred to as hidden imports in PyInstaller terminology.

You can manually specify the hidden imports to force PyInstaller to include those dependencies if changing the imports in your application is too difficult. You’ll see how to do this later in this tutorial.

Once you can launch your application with a Python script outside of your package, you’re ready to give PyInstaller a try at creating an executable.

Using PyInstaller

The first step is to install PyInstaller from PyPI. You can do this using pip like other Python packages:

Language: Shell
$ pip install pyinstaller

pip will install PyInstaller’s dependencies along with a new command: pyinstaller. PyInstaller can be imported in your Python code and used as a library, but you’ll likely only use it as a CLI tool.

You’ll use the library interface if you create your own hook files.

You’ll increase the likelihood of PyInstaller’s defaults creating an executable if you only have pure Python dependencies. However, don’t stress too much if you have more complicated dependencies with C/C++ extensions.

PyInstaller supports lots of popular packages like NumPy, PyQt, and Matplotlib without any additional work from you. You can see more about the list of packages that PyInstaller officially supports by referring to the PyInstaller documentation.

Don’t worry if some of your dependencies aren’t listed in the official docs. Many Python packages work fine. In fact, PyInstaller is popular enough that many projects have explanations on how to get things working with PyInstaller.

In short, the chances of your project working out of the box are high.

To try creating an executable with all the defaults, simply give PyInstaller the name of your main entry-point script.

First, cd in the folder with your entry-point and pass it as an argument to the pyinstaller command that was added to your PATH when PyInstaller was installed.

For example, type the following after you cd into the top-level reader directory if you’re following along with the feed reader project:

Language: Shell