To be able to distribute Python application to end-users you need to freeze Python code. There are numerous freeze tools which produce self-contained binaries for the most common platforms, like Windows, Linux and macOS. This article presents and compares seven freeze tools, which are being actively maintained at the time of writing, and recommends specific tools to use.
Python is a scripting language, where an interpreter compiles your Python code to byte code, just-in-time. This is great for developers, because it allows for rapid application development: you change the code, restart the interpreter, and get results quickly. You don’t need to wait for code compilation to complete (as would be the case for, say, Java or C++).
The downside of this approach is that anyone who wants to run your application also needs a Python interpreter – probably even a specific Python version, with a set of dependencies the user needs to install via
pip. This makes it much more difficult to distribute your application to consumers or end-users who are not tech-savvy.
To avoid this problem there are various tools which “freeze” your Python application, producing binaries that include the Python interpreter, your code (as byte code) and any dependencies. These binaries can be launched on other machines without any further dependencies. This article compares seven of these tools and provides recommendations which one(s) to use.
Distribution and update mechanisms
Tools to freeze Python code
Let’s start with an overview of the available tools:
|Supported platforms||Windows||macOS||Win, macOS, Linux||Win, macOS, Linux||Win, macOS, Linux||Win, macOS, Linux||Win, macOS, Linux||Win, macOS, Linux, Android, iOS|
|Ease of setup||Easy||Easy||Easy||Easy||Medium||Medium||Hard||Hard|
|Auto-discovery of dependencies||✅||✅||✅||✅||✅||❌||❌||❌|
|Support for (native) third party libraries / hooks||Few||Few||Very many||Many||Few||—||—||—|
|Supported Python versions||3.6 – 3.9||3.6 – 3.9||3.5 – 3.9||3.6 – 3.9||2.6 – 2.7|
3.3 – 3.9
|3.8 – 3.9||2.6, 2.7|
3.3 – 3.9
|3.5 – 3.7|
|Maturity & activity||Low|
|Low-medium, 700 stars||Medium,|
(not on GitHub)
|Further limitations||—||—||—||—||Translates Python to C, run-time behavior may differ!|
No macOS bundles
|No macOS bundles,|
Requires compilation for native ext. modules
|No macOS bundles||Only works for PyQt 5/6 apps|
Some of the rows require a few explanations:
- Supported platforms: for which operating systems the freezer tool can build binaries. Note that cross-platform freezing is usually not supported. You have to freeze the macOS binary on macOS, freeze the Windows binary on Windows, etc.
- Ease of setup: How much work it is to get a working system set up:
- Easy: something like
pip install package-nameis sufficient
- Medium: like Easy, but additionally requires installation of a compiler, typically a C compiler like GCC or MSVC
- Hard: like Medium, but additionally requires deep and platform-specific knowledge of C++ and/or compilers to get the freeze process working
- Easy: something like
- Auto-discovery of dependencies: whether the freezer tool automatically follows
importstatements, making it sufficient to just point the tool to your entry point Python script – it will figure out the rest for you
- Support for (native) third party libraries: whether the freezer tool developers built “hooks” that detect third party Python libraries (such as numpy) used in your code. These hooks make sure that the freezing process correctly freezes those library’s files (including dynamic libraries like
sofiles) along with your own application code
- Maturity & activity: maturity refers to the number of years the tool has been in active development, which helps ironing out any bugs or other issues. Activity refers to the number of commits, and the number of contributors. A solution that has just one contributor would be dead in the water if she stopped working on the project
- Low: one contributor, few commits, few GitHub stars
- Medium: one contributor, few to many commits, few to many GitHub stars – the bus factor is still high
- High: multiple contributors, high commit activity, many GitHub stars
py2exe & py2app
py2exe and py2app are two completely independent projects, which have existed for many years, but have had little activity overall. In fact, py2exe was dead between 2014 and late 2020, when development was resurrected again. Both projects are supported by a single contributor. I would rate py2exe’s maturity to be slightly lower than the one of py2app, due to the long inactivity period. Note that both tools are integrated with distutils, but neither of them support Linux.
PyInstaller is an extremely mature project (started in 2006), with steady activity. The main advantages are its ease of use, and a very large number of hooks which ensure that third party libraries (used by your application) are correctly frozen. Unlike py2app or py2exe, PyInstaller supports all major operating systems.
This tool is very similar to PyInstaller. The only difference is that its fan base seems to be smaller (according to the GitHub stars), it has fewer contributors and includes fewer hooks.
Nuitka is actually a Python to C transpiler, which translates Python to C code. This makes Nuitka special in comparison to all other tools (except for Cython, see below), which only compile Python to byte code, and package (zip) that byte code along with a native loader script. Nuitka’s produced C code still depends on Python’s C library, though.
You can use Nuitka to either turn pure Python modules or packages into natively compiled extension modules (e.g. for performance reasons), or to build stand-alone executables. Nuitka is more difficult to use than the previously discussed tools, because you need a C++ compiler. The resulting binaries are just binaries – Nuitka cannot wrap them in common distribution formats, such as macOS app bundles. Another caveat is that the transpiled code might behave slightly different to the pure Python equivalent, which may cause difficult-to-debug run time issues. Nuitka itself is quite mature and has a large fan base, but is supported by only one contributor.
PyOxidizer is a rather new project (since 2019), also supported by only one contributor. To use it, you need a C++ compiler as well as the Rust language run-time. Compared to other tools, the set of supported Python versions is restricted. Like Nuitka, PyOxidizer only produces binaries (no app bundles), and it does not handle those third party libraries that use native extension modules well yet.
The goal of Cython is not to freeze applications, but to help you write native extension modules, based on transpiling. In that sense, it is similar to Nuitka. I only added Cython to this comparison, because it does have an
--embed option (see here), which produces a
main.c file which contains code to start the Python interpreter and make it load your main module. However, you need to figure out the compilation and linking process yourself, and the resulting binary is not always guaranteed to work on other systems. See here for more information. Other disadvantages of Cython are that it does not follow imports, and does not offer any hooks.
pyqtdeploy is built by the same author (Phil Thompson) who also built PyQt 5/6, and only works for Python apps using PyQt. Its community is quite small, and there are very few tutorials beyond the official manual to get started. You not only need a C++ compiler, but should also be familiar with compiling Qt applications, because that is the process being used under the hood. On the plus side, pyqtdeploy is the only tool that supports building binaries for mobile platforms (iOS, Android), assuming that you are C++ wizard who knows all the right compiler flags for cross-compilation, and can solve linker issues with ease.
As with any technology you integrate into your application’s tech stack, you need to consider the day 1 and day 2-n costs. Day 1 costs correspond to the time it takes to learn and adopt the solution (see easy of use), and day 2-n costs refer to the time you spend maintaining the technology over a longer period of time (here: keeping your build script that invokes the freezer tool working). Day 1 costs are very high, and using a mature solution not only reduces day 2-n costs, but also ensures that you don’t have to pay day 1 costs multiple times, e.g. because the solution you previously used stopped working and now you have to pick up a new one.
For this reason, I would generally recommend to use PyInstaller, simply because it is more mature than other solutions. It also has many features and is easy to use, with lower day 1 costs than others. The only downside of PyInstaller (and cx_freeze) is that it produces somewhat large frozen application folders, in terms of file size and the number of files. Expect that you need to spend time on fine-tuning PyInstaller to exclude specific packages (third party, or yours), which PyInstaller’s hooks or follow-import mechanism thought your application would be using, but actually are not used.
To actually distribute your application to your users, these tools just cover the first necessary “compilation” step. In separate articles you can learn how to package frozen applications, e.g. as installer (Windows), or disk image (macOS).