Bundling a Python application in a single binary with box and PyApp

Published on · Last updated: · Edit on Github · llms.txt

#python

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 of PyApp), 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:

  1. Compiles a minimal binary with:
    • a minimal Python runtime (via Cython)
    • a package manager (uv)
    • a list of your project dependencies
  2. On first launch, the binary unpacks the runtime and installs your dependencies locally (just once)
  3. On subsequent runs, it reuses the cached environment and launches instantly
Tip

Need to update dependencies later? No need to rebuild the binary—just run self update.

Figure 1: How PyApp Works
Figure 1: How PyApp Works Edit on

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/.

Note

The 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.

Note

The 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.

Figure 2: Tauri Sidecar Architecture
Figure 2: Tauri Sidecar Architecture Edit on

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)
Warning

The 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).

Note

The platform triple in the filename helps Tauri select the correct binary for each OS/architecture.

A *platform triple* (`x86_64-apple-darwin`, etc.) identifies OS + architecture. It ensures the right binary runs on the right system.

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.

Important

Always regenerate your TypeScript client after making changes to your FastAPI endpoints or models to keep types in sync.

How to use it:

  1. Generate the client:

    			bunx@hey-api/openapi-ts-iopenapi.json-osrc/lib/python/client-c@hey-api/client-fetch
    		
  2. 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
Tip

Use 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:

Footnotes

  1. PEP 621 defines a standardized way to declare metadata like name, version, and dependencies in pyproject.toml. ↩︎

Comments

Socials

Links

Miscellaneous

  1. [1] All opinions are my own, except those generated by large language models.
  2. [2] Fonts: ...
Guybrush.ink
Made with in Paris, London & Toulouse build: main