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
  • A build folder
    • The build folder is where PyInstaller 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.
  • 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

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!