Python, powerful and versatile as it is, lacks a few key capabilities out of the box. For one, Python provides no native mechanism for compiling a Python program into a standalone executable package.
To be fair, the original use case for Python never called for standalone packages. Python programs have, by and large, been run in-place on systems where a copy of the Python interpreter lived. But the surging popularity of Python has created greater demand for running Python apps on systems with no installed Python runtime.
Several third parties have engineered solutions for deploying standalone Python apps. The most popular solution of the bunch, and the most mature, is PyInstaller
. PyInstaller
doesn’t make the process of packaging a Python app to go totally painless, but it goes a long way there.
Waitress WSGI Server
Waitress is a pure-Python WSGI server. At a first glance it might not appear to be that much different than many others; however, its development philosophy separates it from the rest. Its aim for easing the production (and development) burden caused by web servers for Python web-application developers.
The installation is pretty simple. It is highly recommended to create a virtual environment before you install Waitress via the pip install command:
pip install waitress
Then You need to first import waitress via the following command:
from waitress import serve
I will be using the app as the variable name for the Flask server. Modify this according to the name that you have set:
app = Flask(__name__)
Comment out the app.run in your main server and add the following code.
By default, Waitress binds to any IPv4 address on port 8080. You can omit the host and port arguments and just call serve with the WSGI app as a single argument. we overwrite it and set the port
to 5000
for demostration on how to change them.
serve( app.run() host="127.0.0.1", port=5000, threads=2 )
The most commonly-used parameters for serve are as follows:
host
— Hostname or IP address (string) on which to listen, default 127.0.0.1, which means “all IP addresses on this host”. May not be used with the listen parameter.port
— TCP port (integer) on which to listen, default 8080. May not be used with the listen parameter.ipv4
— Enable or disable IPv4 (boolean).ipv6
— Enable or disable IPv6 (boolean).threads
— The number of threads used to process application logic (integer). The default value is 4.
Create Executable from Python Script using Pyinstaller
PyInstaller
can be used to create .exe files for Windows, .app files for Mac, and distributable packages for Linux. Optionally, it can create a single file which is more convenient for distributing, but takes slightly longer to start because it unzips itself.
The installation is pretty simple. It is highly recommended to create a virtual environment before you install via the pip install command.
serve( app.run() host="127.0.0.1", port=5000, threads=2 )
This file is saved in build.sh and runs this file using the following command in the terminal.
serve( app.run() host="127.0.0.1", port=5000, threads=2 )
For windows is:
serve( app.run() host="127.0.0.1", port=5000, threads=2 )
For mac is:
serve( app.run() host="127.0.0.1", port=5000, threads=2 )
For ubuntu is:
serve( app.run() host="127.0.0.1", port=5000, threads=2 )
The most commonly-used parameters for build.sh
are as follows:
--name
— Change the name of your executable.--onefile
— Package your entire application into a single executable file. The default options create a folder of dependencies and an executable, whereas –onefile keeps distribution easier by creating only an executable.--hidden-import
— List multiple top-level imports that PyInstaller was unable to detect automatically. This is one way to work around your code using import inside functions and import(). You can also use –hidden-import multiple times in the same command.--add-data
and--add-binary
— Instruct PyInstaller to insert additional data or binary files into your build. This is useful when you want to bundle in configuration files, examples, or other non-code data.
PyInstaller
is complicated under the hood and will create a lot of output. So, it’s important to know what to focus on first. Namely, the executable you can distribute to your users and potential debugging information. By default, the pyinstaller command will create a few things of interest:
- A
*.spec
file- where all configuration was put by
pyinstaller
- where all configuration was put by
- A
build
folder- The
build
folder is wherePyInstaller
puts most of the metadata and internal bookkeeping for building your executable. - The
build
folder can be useful for debugging, but unless you have problems, this folder can largely be ignored.
- The
- A
bin
folder- will be created After building, you’ll end up with a bin folder similar to the following:
bin/ | └── our_app/ └── our_app # this is the executable
- will be created After building, you’ll end up with a bin folder similar to the following:
The bin
folder contains the final artifact you’ll want to ship to your end users. Inside the bin
folder, there is a folder named after your entry-point. So in this example, you’ll have a bin/our_project
folder that contains all the dependencies and executable for our application.
The executable to run is bin/our_app/our_app
or bin/our_app/our_app.exe
if you’re on Windows.
pyInstaller creates a temp folder and the name of that folder is __MEIPASS__
. Generally a new e.g. __ME<Random_Value>__
file created at the time of each time we execute the file and previous __MEIPASS__
file deleted because of it’s volatile memory. So the previous data is removed from storage as we store our db and other files in the that temp folder using pyinstallers --add-data
property, but we need to store previous data for the persistence. For this reason we create a hidden folder in the system’s home directory and store data in this folder. But initially sqlite database file does not exist in this hidden folder. So at execution time we create a hidden folder in the system home directory when we execute the file and we have to copy that fresh db along with other files from the temp folder and save to the hidden folder. The code of copying and saving this db along with the other files given below:
import os, shutil from pathlib import Path from .resources import get_resources_path APP_NAME = "our_app" HOME_DIR = Path.home() APP_DIR = HOME_DIR / f".{APP_NAME.lower()}" if not APP_DIR.exists(): ## checking if our persistence hidden filder exists or not os.mkdir(APP_DIR) ## create the hidden folder data = get_resources_path() / data ## searching files in the temp folder if not (APP_DIR / data).exists(): ## checking if our persistence files already in the hidden directory or not try: shutil.copy(data, APP_DIR) except Exception as e: log.exception(e)
Get resources path function to find the
__MEIPASS__
folder path link from where we can copy fresh data and can store to the hidden folder.
import pathlib import sys def get_resources_path(relative_path="."): rel_path = pathlib.Path(relative_path) prod_base_path = pathlib.Path(__file__).resolve().parent.parent try: # PyInstaller creates a temp folder and stores path in _MEIPASS base_path = sys._MEIPASS except Exception as e: base_path = getattr(sys, "_MEIPASS", prod_base_path) return base_path / rel_path
Conclusion
PyInstaller can help make complicated installation documents unnecessary. Instead, your users can simply run your executable to get started as quickly as possible. The PyInstaller workflow can be summed up by doing the following:
- Create an entry-point script that calls your main function.
- Install PyInstaller.
- Run PyInstaller on your entry-point.
- Test your new executable.
- Ship your resulting dist/ folder to users.
Your users don’t have to know what version of Pyt hon you used or that your application uses Python at all!