Bundling a Python application in a single binary with box and PyApp
Published on · Last updated: · Edit on Github · llms.txt
Bundling Python applications into a single, portable binary has long been tricky—especially if you want users to just download and run your tool, without needing to install Python, set up virtual environments, or fight dependency issues. Thanks to Reto Trappitsch (@trappitsch) and his
box
tool (built on top ofPyApp
), this process is finally approachable.
In this post, I’ll show how to use box
to bundle Python inference logic (e.g., for Nougat) as a sidecar in a Tauri desktop app—and how to get type safety between Python and TypeScript using OpenAPI.
Why bundle Python?
Shipping a single binary has several advantages:
- 🐍 No Python required on the user's machine
- 💻 Cross-platform builds (Linux, macOS, Windows)
- 🧳 Easy integration as a Tauri sidecar
- 🧩 Reproducibility — you ship exactly the Python version and dependencies you want
box
is a thin wrapper around PyApp
, designed to make this whole process smooth and developer-friendly.
How does PyApp work?
Unlike traditional bundlers like PyInstaller or shiv, PyApp doesn’t package Python and your dependencies all together at build time. Instead, it:
- Compiles a minimal binary with:
- a minimal Python runtime (via Cython)
- a package manager (
uv
) - a list of your project dependencies
- On first launch, the binary unpacks the runtime and installs your dependencies locally (just once)
- On subsequent runs, it reuses the cached environment and launches instantly
TipNeed to update dependencies later? No need to rebuild the binary—just run
self update
.
Step-by-step: Bundling your Python project
1. Prerequisites
You’ll need:
- A Python project using
pyproject.toml
(PEP 6211) - Rust and Cargo (PyApp uses them under the hood)
- uv as your Python package manager (optional, but recommended)
2. Configure box in your pyproject.toml
Add box
as a development dependency:
Shell
uvadd--devbox-packager
Then configure it in your pyproject.toml:
pyproject.toml
[project] name="my_project" version="0.1.0" requires-python=">=3.12" [project.scripts] my_app="my_package.main:main" [dependency-groups] dev=["box-packager>=0.4.0"] [tool.box] builder="hatch"#orrye,pdm,etc. is_gui=false app_entry="my_package.main:main" entry_type="spec" python_version="3.12" [tool.box.env-vars] PYAPP_UV_ENABLED="true" PYAPP_EXPOSE_METADATA="false"
Then run box init
to initialize your project:
Shell
uvx--frombox-packagerboxinit
Tip
Use dev dependencies for build tools like box to keep your project reproducible and easy to set up for collaborators.
3. Build the binary
This will package your Python code and dependencies into a single executable:
Shell
uvx--frombox-packagerboxpackage
This builds your Python project, downloads PyApp, and packages everything into a single binary using Cargo. The resulting executable will be in target/release/
.
NoteThe first build may take a while, as Cargo compiles a custom binary for your project.
You might need to run chmod +x target/release/{my_project}
to allow execution.
4. Run your bundled application
Once built, just run the binary:
Shell
./target/release/{my_project}
On first run, this creates a virtual environment, install dependencies, and launches your entrypoint. Subsequent runs use the cached environment.
NoteThe virtual environment is created in a user-specific directory (e.g.,
~/Library/Application Support/pyapp/{my_project}/{uuid}/{version}/
on macOS).
5. Useful commands
PyApp exposes commands to manage the virtual environment:
./target/release/{my_project} self remove
: Remove the virtual environment./target/release/{my_project} self restore
: Remove and reinstall the virtual environment./target/release/{my_project} self update
: Update the virtual environment./target/release/{my_project} self python
: Open an interactive Python shell with the virtual environment
Tip
Use self python
to debug or inspect your environment interactively.
Use case: a Python sidecar in Tauri
Note
This section is useful if you want to use box
to bundle a Python application as a sidecar in a Tauri application.
Let’s say you’re building a desktop app (for example, with Tauri) and you want to run some Python logic—like ML inference behind the scenes. It's a pain to get all the dependencies and Python version right for your users, so you decide to bundle Python with your app.
In Montelimar, I use box
to bundle Python inference logic for Nougat as a sidecar. In Tauri, a sidecar is an external binary shipped alongside your main Rust/JS app, often used for tasks like ML inference or system integration.
How it works:
- The Python FastAPI server is bundled as a single binary with
box
- The Tauri app launches this binary as a sidecar process
- Communication happens over HTTP (localhost)
WarningThe Tauri application process is responsible for creating and cleaning up the sidecar. If you forget to terminate the sidecar, it may keep running in the background.
Example build script (from package.json
):
package.json
{ //... "scripts":{ //... "python:package:build":"uvx--directorysrc-python--frombox-packagerboxpackage&&mkdir-psrc-tauri/binaries&&forfileinsrc-python/target/release/*;doext=${file##*.};[\"$file\"=\"$ext\"]&&ext=\"\"||ext=.$ext;dest=\"src-tauri/binaries/$(basename${file%.*})-$(rustc-Vv|grephost|cut-f2-d'')$ext\";cp-f\"$file\"\"$dest\";chmod+x\"$dest\";done", } }
This script builds the Python binary and copies it to the Tauri binaries folder, renaming it with the platform triple (e.g., nougat-linux-x86_64
).
A *platform triple* (`x86_64-apple-darwin`, etc.) identifies OS + architecture. It ensures the right binary runs on the right system. NoteThe platform triple in the filename helps Tauri select the correct binary for each OS/architecture.
Type-safe integration: OpenAPI and TypeScript clients
To ensure type safety between the Python FastAPI backend and the TypeScript frontend, export the OpenAPI schema with FastAPI and generate a TypeScript client with @hey-api/openapi-ts
:
Python: Export OpenAPI
src/export_openapi.py
importjson frompathlibimportPath fromfastapi.openapi.utilsimportget_openapi fromocr_mlx.endpointimportapp defexport_openapi(path:Path): withpath.open("w",encoding="utf-8")asf: json.dump( get_openapi( title=app.title, version=app.version, openapi_version=app.openapi_version, description=app.description, routes=app.routes, ), f, ) if__name__=="__main__": export_openapi(Path("openapi.json"))
And generate the OpenAPI schema:
Shell
pythonsrc/export_openapi.py
Generate TypeScript client:
Shell
bunx@hey-api/openapi-ts-iopenapi.json-osrc/lib/python/client-c@hey-api/client-fetch
This produces fully-typed TypeScript definitions and fetch clients, ensuring your frontend and backend stay in sync.
ImportantAlways regenerate your TypeScript client after making changes to your FastAPI endpoints or models to keep types in sync.
How to use it:
- Generate the client:
bunx@hey-api/openapi-ts-iopenapi.json-osrc/lib/python/client-c@hey-api/client-fetch
- Import and use the generated functions in your frontend:
import{someApiEndpoint}from'@/python/client/sdk.gen'; //Exampleusage someApiEndpoint({ body:{.../*yourrequestdatafullytyped!*/}, baseUrl:'http://localhost:7771', //fetch:customFetch//e.g.,window.fetch,tauriFetch,etc. }).then((res)=>{ //resultsdataisalsofullytyped! console.log(res.data); });
Benefits:
- Type safety: All parameters and responses are fully typed
- Sync with backend: The generated client stays up-to-date as your API evolves
- Flexible: Works with any fetch implementation and integrates easily into your frontend codebase
TipUse your editor's autocompletion and type checking to catch API mismatches early!
Conclusion
With box and PyApp, bundling Python apps is no longer a dark art. Whether you’re building CLI tools or integrating Python into a cross-platform desktop app, this toolchain delivers fast builds, clean binaries, and zero-friction setup for your users.
And if you pair it with OpenAPI-generated clients, you get a full-stack Python-to-TypeScript bridge with type safety all the way down.
Links: