Distributing a Python program to Windows users

I use Python for several tasks (mostly small routine jobs or one-off scripts; not to devalue the language, I just haven’t used it yet for bigger projects), but since it’s an interpreted language, you can’t easily distribute the scripts to normal users on Microsoft Windows (my main ecosystem), because that platform doesn’t come with any version of Python installed by default.

That means you’d have to convince and explain to the user why and how to install an extra software, just so he or she can run your ten lines of code — a burden on both sides.
So when I searched around for hints for a solution, I came accross the technique of freezing your code:

“Freezing” your code is creating a single-file executable file to distribute to end-users, that contains all of your application code as well as the Python interpreter.

First I tried py2exe, which didn’t work as I hoped; maybe rather a problem of my limited time spent reading the documentation or forums or wikis… I did not test everything, for example I skipped cx-freeze because it doesn’t offer an ‘one file’ mode. When it came to testing PyInstaller, that was very easy (after minor issues at the start, see below) and fullfilled my requirements, so I stuck with it, for now.

Additionally, a GUI is often necessary, especially for end-users that are often not accustomed to the command line. Again, searching for viable solutions, I hit upon multiple frameworks, the most prominent being Qt and WxWidgets. And although I already have experience with Qt (under Windows and with C++), those toolkits tend to bloat the applications significantly; again, not too good for a small program.

Fortunately, Python has a de-facto standard GUI with Tk, that is installed as TkInter by default. And while its programming model seems rather arcane and I wouldn’t wish to use it extensively, it’s easy enough for simpler things and the widgets don’t look too out of place on Windows.

Creating a little Tkinter script

This is the source code for a little Tk GUI test script, save it as script.py under something like C:\MyScript. You can run it from a prompt and it will open a little window with some input fields and a button.

Named with the extension *.py, a console will also be open in the background; if the file is renamed with the *.pyw suffix instead, only the GUI window will be shown — PyInstaller takes care of that with an option (described below).

from Tkinter import * # for Python 2.x; use 'tkinter' (lower case) for Python 3.x.

def addValues():
	label_value.config(text=float(entry_1.get()) + float(entry_2.get()))

root_widget = Tk()

label_1 = Label(master=root_widget, text='Value 1:')
label_1.grid(row=0, column=0, padx=5, pady=5, sticky=W)

entry_1 = Entry(master=root_widget)
entry_1.grid(row=0, column=1, padx=5, pady=5)

label_2 = Label(master=root_widget, text='Value 2:')
label_2.grid(row=1, column=0, padx=5, pady=5, sticky=W)

entry_2 = Entry(master=root_widget)
entry_2.grid(row=1, column=1, padx=5, pady=5)

button_action = Button(master=root_widget, text='Add both values', command=addValues)
button_action.grid(row=2, columnspan=2, padx=5, pady=5)

label_result = Label(master=root_widget, text='Sum:')
label_result.grid(row=3, column=0, padx=5, pady=5, sticky=W)

label_value = Label(master=root_widget, text='')
label_value.grid(row=3, column=1, padx=5, pady=5)


Installing PyInstaller

I’m assuming, for simplicity’s sake, that PIP (the built-in package management system of Python) is available. It’s installed by default since Python 2.7.9 and Python 3.4; on older versions, you can add it manually (please look it up in the Python documentation yourself).

Alternatively, you can download PyInstaller directly from its website.

Open a command prompt and enter:
C:\> pip install pyinstaller

Note: If the UAC is enabled on your machine and Python is installed under C:\Program Files or C:\Program Files (x86), then open the command prompt as an administrator, otherwise PIP probably doesn’t have enough access right to modify the folder’s content and will complain halfway through the installation.

Using PyInstaller

When your are done with it, switch to the directory where your source files lives or where you want the output of PyInstaller to be generated, e.g. C:\MyScript.

There you can start a test run:
C:\MyScript> pyinstaller Script.py

… which might crash and burn, as it did in my case, with an error message: “Failed to create process”.

Luckily, this seems to be an old and common problem (that still hasn’t been solved): If Python is installed on path with spaces in it (for example C:\Program Files (x86)\Python), then the PyInstaller scripts can’t use/find it without some adjustments.

On that StackOverflow page referencend above, two workarounds are proposed (the first one is not recommended):

  1. Edit one line in several files in your Python directory. The problem with this solution: If you update PyInstaller, or uninstall and reinstall it again in the future, those changes will be lost! (Good luck remembering then that you once had to edit several files…):

    1. Open C:\Program Files (x86)\Python\2.7\Scripts
    2. Open all pyi-*.py files and the file pyinstaller-script.py with an editor (if UAC is enabled on your machine, you’ll have to run the editor with administrator rights to save the changes in the files here).
    3. In each of those files, quote the path on the first line (i.e. adding “…"),
      so that #!c:\program files (x86)\python\2.7\python.exe
      becomes #!"c:\program files (x86)\python\2.7\python.exe"
  2. This more verbose, but future proof (and maybe you can wrap it in batch file or something for easier handling).
    Instead of calling
    C:\MyScript> pyinstaller Script.py
    you’ll have to call the Python interpreter and PyInstaller script with its full (quoted) path:
    C:\MyScript> "C:\Program Files (x86)\Python\2.7\python.exe" "C:\Program Files (x86)\Python\2.7\Scripts\pyinstaller-script.py" script.py

Fine-tuning PyInstaller

When running PyInstaller on your file(s), it will gather all required files and dependencies and the output will be in C:\MyScript\dist\script, which will contain maybe 20+ files and folders (including a script.exe), weighting approximately 12-15 Megabytes on my system.

Also, when running script.exe, the console will pop-up again, as mentioned before.

So this state is still not what I was looking for; I had to add two options to the PyInstaller command line:
C:\MyScript> "C:\Program Files (x86)\Python\2.7\python.exe" "C:\Program Files (x86)\Python\2.7\Scripts\pyinstaller-script.py" script.py --onefile --windowed

The result

The result is a single file named script.exe under C:\MyScript\dist\script which is ca. 5 MBytes of size and that you can distribute to any Windows user, without them having to install the Python interpreter first.

Admittedly, 5 MB instead of 2 KB for the pure source code file (script.py) is a lot, but that’s the cost of the ease of operation for the user and installing the Python runtime isn’t without space requirments neither. Of course, if you’re starting you use dozens of such programs, then installing Python on that machine would make more sense…

A final note: Just because the script is now wrapped in an EXE does not mean, the source code is very safe. PyInstaller includes the compiled byte code files (.pyc) and not the original source code files (.py), but there are decompilers available that will turn byte code back into equivalent source code!