Written in Rust, the PyApp utility wraps up Python programs into self-contained click-to-run executables. It might be the easiest Python packager yet. Credit: Cybrain / Getty Images Every developer knows how hard it is to redistribute a Python program as a self-contained, click-and-run package. There are third-party solutions, but they all have drawbacks. PyInstaller, the oldest and best-known tool for this job, is crotchety to work with and requires a fair amount of trial-and-error to get a working redistributable. Nuitka, a more recent project, compiles Python programs to redistributable binaries, but the resulting artifacts can be massive and take a long time to produce. A newer project, PyApp, takes an entirely different approach. It’s a Rust program you compile from source, along with information about the Python project you want to distribute. The result is a self-contained binary that, when run, unpacks your project into a directory and executes it from there. The end user doesn’t need to have Python on their system to use it. Setting up PyApp Unlike other Python distribution solutions, PyApp is not a Python library like PyInstaller. It’s also not a standalone program that takes in your program and generates an artifact from it. Instead, you create a custom build of PyApp for each Python program you want to distribute. You’ll need to take care of a few prerequisites before using PyApp to deploy Python programs: PyApp’s source: Make a clone of PyApp’s source and put it in a directory by itself, separate from any other projects. The Rust compiler and any other needed infrastructure: If you’re unfamiliar with Rust or its tooling, you’ll need to understand at least enough of it to compile a Rust program from source. Check out my tutorial for getting started with Rust to see what you need. Your Python program, packaged as a wheel: The “wheel,” or .whl file, is the binary format used to package Python programs along with any platform-specific components (such as precompiled libraries). If you don’t already have a wheel for the Python program you want to repackage, you’ll need to generate one. My video tutorial for creating Python wheels steps you through that process. You can also use wheels hosted on PyPI. PyApp uses environment variables during the build process to figure out what Python project you want to compile into it and how to support it. The following variables are the ones most commonly used: PYAPP_PROJECT_NAME: Used to define the name of the Python project you’ll be bundling. If you used pyproject.toml to define your project, it should match the project.name attribute. This can also be the name of a project on PyPI; if not, you will want to define the path to the .whl file to use. PYAPP_PROJECT_VERSION: Use this to configure a specific version of the project if needed. PYAPP_PROJECT_PATH: The path (relative or absolute) to the .whl file you’ll use for your project. Omit this if you’re just installing a .whl from PyPI. PYAPP_EXEC_MODULE: Many Python packages run automatically in some form when run as a module. This variable lets you declare which module to use, so if your program runs with thisprogram, you’d set this variable to: python -m thisprogram. PYAPP_EXEC_SPEC: For programs that have an entry-point script, you can specify it here. This matches the syntax in the project.scripts section of pyproject.toml. For instance, pyprogram.cmd:main would import the module pyprogram.cmd from your Python program’s modules, then execute the function main() from it. PYAPP_EXEC_SCRIPT: This variable lets you supply a path to an arbitrary Python script, which is then embedded in the binary and executed at startup. PYAPP_DISTRIBUTION_EMBED: Normally, when you create a PyApp binary, it downloads the needed Python distribution to run your program from the Internet when it’s first executed. If you set this variable to 1, PyApp will pre-package the needed Python distribution in the generated binary. The result is a larger binary, but one that doesn’t need to download anything; it can just unpack itself and go. Many other options are available, but these should be enough for most projects you want to build. To make things easy on yourself, you may want to create a shell script for each project that sets up these environment variables and runs the compilation process. Building the PyApp binary Once you’ve set the environment variables, go to the root directory of PyApp’s source, and build PyApp using the Rust compiler with the command: cargo build --release This might take several minutes, as PyApp has many dependencies (363 as of this writing). Future compilation passes will take much less time, though, once Rust obtains and caches everything. Note that while it’s possible to cross-compile for other platforms, it is not recommended or supported. Once the compiling is done, the resulting binary will be in the target/release subdirectory of the PyApp project directory, as pyapp.exe. You can rename the resulting file anything you want, as long as it’s still an executable. Running the PyApp binary To test the binary, just run it from a console. If all is well, you should see prompts in the console as PyApp unpacks itself and prepares to run. If you see any failures in the console, take note of them and edit your environment variables; chances are you didn’t properly set up the entry point or startup script for the project. When the PyApp binary runs for the first time, it’ll extract itself into a directory that’s usually a subdirectory of the user profile. On future runs, it’ll use that already-unpacked copy and so will launch much faster. If you want to control where the binary unpacks itself, you can set an environment variable to control where the program writes and looks for its unpacked contents when it’s run: PYAPP_INSTALL_DIR_<project_name> = "path/to/directory" Note that <project_name> is an uppercased version of the PYAPP_PROJECT_NAME variable. So, for the program conwaylife, we’d use the variable name PYAPP_INSTALL_DIR_CONWAYLIFE. The directory can be a full path or a relative one, so you could use a directory like ./app to indicate the application should be unpacked into the subdirectory app of the current working directory. Also note that this setting is not persistent. If you don’t have this exact environment variable set when you run the program, it’ll default to the user-profile directory. PyApp options The deployed PyApp executable comes with a couple of convenient commands built into it: pyapp self remove: Removes the unpacked app from the directory it was unpacked into. pyapp self restore: Removes and reinstalls the app. Note again that if you used the PYAPP_INSTALL_DIR_ variable to set where the project lives, you must also have it set when running these commands! It’s also important to note that PyApp-packaged apps can generate false positives with antivirus solutions on Microsoft Windows, because the resulting executables aren’t code-signed by default.