Python - Packaging Guide
A Model Example
See my mock application - the Auto League Closer™ - for a real-world demo (with external libraries, data files, and application logs) of how to package, freeze, or make a windows installer for your Python project. This model application was also made to follow best practices for Python projects in general!
Packaging Python Projects
Scenario: You want to package your code and upload it onto PyPi so end users can just pip install
it.
How do you make your code into a package? See this tutorial.
- Create a
pyproject.toml
file in the same directory as the module file or folder. Fill it with the following info - change default fields as needed and ensure the project name matches the module name:
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
[project]
name = "my_amazing_package"
version = "0.0.1"
authors = [
{ name="Example Author", email="author@example.com" },
]
description = "A small example package"
readme = "README.md"
requires-python = ">=3.7"
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
]
dependencies = [
"numpy>=1.13.3",
]
[project.urls]
"Homepage" = "https://github.com/pypa/sampleproject"
"Documentation" = "https://package.readthedocs.io/"
- Run the below commands to build your package:
$ pip install build
$ python -m build
- Upload your package to PyPi:
pip install twine
python -m twine upload dist/*
This official Python 3 tutorial covers it all in a surprisingly simple way (use setuptools because everyone else is). This article covers how to include dependencies. I don’t want to reinvent the wheel by explaining all the details, so take a look at that.
Freezing Python Projects
Scenario: You want to package your entire Python program - external dependencies and all - into a single EXE file that others can run, without installing the program first (so it’s a standalone executable).
Use PyInstaller. It’s more mature and widely used/maintained than all the other options out there. See this article for pretty much all the options you’ll ever need for any project setups.
$ pip install pyinstaller
# Create one directory in the `dist` folder containing an executable
# Think of pyinstaller like this: When you run the generated EXE,
# it does whatever `python entry.py` would do, and nothing more.
$ pyinstaller entry.py --name "My Cool Program" --icon MY_APP_ICON.png
# Bundle the entire Python program into one standalone EXE
# Note that this EXE will take a long time to launch! (It extracts the folder on every run)
$ pyinstaller entry.py --onefile --name "My Cool Program" --icon MY_APP_ICON.png
# Fix PyInstaller if your program requires external data files
# Say you're running pyinstaller OUTSIDE of the module folder "my_module"
# and you need a bunch of images in a resource folder located at "my_module/data"
# while preserving any relative paths used by your Python code
$ pyinstaller entry.py --add-data "my_module/data;data" --name "My Cool Program" --icon MY_APP_ICON.png
There’s a different way to collect data files that works without manually specifying all of them. It’s really simple too, so if you have more than one data file/folder or the data folder has a long path, use this method! It’s given in the 2nd half of this article.
Making a Windows App Installer
Scenario: You want to make an app like Discord or Google Chrome. Users need to install it first with a standalone installer, then they can run it however they want.
Use Inno Setup on top of Pyinstaller.
- Freeze the Python project (see above).
- Install Inno Setup (yes it’s kinda meta, but install it like you would any other app). Launch it.
- Use the Inno Setup Script Wizard to make an installation script. Select the EXE from PyInstaller, and the folder which contains that EXE as an additional required folder.
- Save the script, compile it, and try out the generated installer!
Note: If you’re trying to create files of some sort in your app (ex: for logging), you’ll need to move those into a different folder, separate from the application, for security reasons. This is what AppData/Roaming
is for! (See this article)
As an extension to the above, you can make your program behave differently in source and bundled locations using PyInstaller’s generated run-time information:
import sys
if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):
print('running in a PyInstaller bundle')
else:
print('running in a normal Python process')
See their tutorial article for details.
Last updated: 09 April 2023