Kemp’s Blog

A technical blog about technical things

Packaging a Python app for Windows

This post gives an overview of the steps to package a Python application for distribution to Windows users in such a way that they don’t have to worry about installing Python, additional modules, and so on. This is significantly more convenient if they are not an experienced computer user, or if they simply have no need for Python otherwise. My particular approach is based around a combination of PyInstaller (to create a Windows executable) and NSIS (to create the installer). I’m not an expert in this and in fact this is my first experimentation outside of py2exe, so your own judgement should be applied. However, this technique worked for me and seems to have been used by people previously (for example, this Stack Overflow answer).

PyInstaller

The first step is to install PyInstaller. Follow the instructions on their website to install. You can install either via pip or by using the archive download. I installed via pip myself as I already had it available. If possible, I would recommend this. Note that in Windows you must install PyWin32 (definitely when using pip, I haven’t tested via the archive). Running the verify step for PyInstaller will warn you if you have forgotten to install PyWin32.

For the purpose of this example, I will assume you have a script called my_script.py that you want to distribute.

Open up a command line, navigate to the directory where your script resides, and run pyinstaller -w my_script.py. All being well, this should succeed and will generate (among many other things) build/my_script/my_script.exe. It is possible to run this, but any resources such as image files will be missing from the build, so expect errors.

In order to include these additional files, you will need to edit the my_script.spec file that was generated when you ran the previous command. The easiest way to do this is to create a list of the additional files you need and then pass that into the COLLECT object that is created:

additional_files = [
    ('image1.png', 'image1.png', 'DATA'),
    ('image2.png', 'image2.png', 'DATA'),
    ('photo.jpg',  'photo.jpg',  'DATA'),
]
          
coll = COLLECT(exe,
               a.binaries,
               a.zipfiles,
               a.datas,
               additional_files,
               strip=None,
               upx=True,
               name='my_script')

For details of the elements in the tuple, check out the PyInstaller spec file documentation.

You can now run PyInstaller and pass it the name of the spec file instead of your script:

pyinstaller my_script.spec

If you get annoyed with it asking your permission to replace the previous build each time then you can silence it with the -y parameter:

pyinstaller -y my_script.spec

In the relatively simple case, that just about wraps it up for PyInstaller and build/my_script/my_script.exe should now run with no problems. If your case is more complicated then you will need to read the spec file documentation and edit your spec file appropriately.

NSIS

The next step is to download and install NSIS. It would be a good idea to scan through some NSIS tutorials to become familiar with the concepts.

We will start with the example NSIS script shown here. From this, the main thing that needs changing is to include all the files from our PyInstaller build. It is possible to use a line such as

File /r "dist\my_script\"

to do this. This technique works correctly for installation, however the uninstaller doesn’t know which files to uninstall (and the same method cannot be used to specify files there). Instead, we will use a helper script to build the file list so that both the installer and uninstaller can be provided with the complete list of files. The helper script is Python-based and is discussed here. Once you have that script available, you need to remove the example lines

file "app.exe"
file "logo.ico"

and replace them with the following

!include ${INST_LIST} ; the payload of this installer is described in an externally generated list of files

Similarly, for the uninstaller specification you need to remove the following lines

delete $INSTDIR\app.exe
delete $INSTDIR\logo.ico

and replace them with

; Remove the files (using externally generated file list)
!include ${UNINST_LIST}

Finally, you need to include a few defines at the top of your NSIS config so that the correct files are used:

!define FILES_SOURCE_PATH dist\my_script
!define INST_LIST install_list.nsh
!define UNINST_LIST uninstall_list.nsh

The final step is to ensure that you run this command after PyInstaller but before NSIS:

python create_file_list.py dist\my_script install_list.nsh uninstall_list.nsh

Conclusion

To summarise, the final sequence of scripts is:

pyinstaller -y my_script.spec
python create_file_list.py dist\my_script install_list.nsh uninstall_list.nsh

Followed by building the NSIS installer. I use the GUI for the final NSIS step, but it can also be run from the command line if you are so inclined.

That should be all you need in order to successfully package your Python program to distribute to Windows users. I hope this gives you a good launching-off point to progress to the more advanced options available in PyInstaller and NSIS.

Appendix A: Add/Remove entry

Some edits to make the Add/Remove entry nicer (more consistent with other entries) and tweak for project name:

WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${COMPANYNAME} ${APPNAME}" "DisplayName" "${APPNAME} (${COMPANYNAME})"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${COMPANYNAME} ${APPNAME}" "DisplayIcon" "$\"$INSTDIR\my_script.ico$\""
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${COMPANYNAME} ${APPNAME}" "Publisher" "${COMPANYNAME}"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${COMPANYNAME} ${APPNAME}" "DisplayVersion" "${VERSIONMAJOR}.${VERSIONMINOR}"

I also removed the following lines (though you may want to keep them):

WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${COMPANYNAME} ${APPNAME}" "HelpLink" "$\"${HELPURL}$\""
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${COMPANYNAME} ${APPNAME}" "URLUpdateInfo" "$\"${UPDATEURL}$\""
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${COMPANYNAME} ${APPNAME}" "URLInfoAbout" "$\"${ABOUTURL}$\""

Appendix B: Relative paths

One thing to bear in mind is that when your program is run from the Start menu (or other ways than double-clicking on the exe itself), any relative paths you use to open files will fail. There are several solutions discussed in the answers to a Stack Overflow question, but the simplest for me was to use something of the form

os.path.join(sys.path[0], 'image.png')