# PyApp: shipping a Python app inside a Rust binary that unpacks itself at runtime

> PyApp is a Rust wrapper that embeds a Python distribution into your application at build time and bootstraps it when the user runs the binary, so an end user without Python still gets a working application. The same binary is produced per platform, and the runtime decides whether to execute code, a module, a script, a spec or a notebook.

**ofek/pyapp** — Runtime installer for Python applications

- Repository: https://github.com/ofek/pyapp
- Website: https://ofek.dev/pyapp/
- Stars: 2,045 · Forks: 68
- Language: Rust
- License: not declared
- Published: 2026-10-13 · Updated: 2026-10-13 · Language: en
- Canonical page: https://hysenlabs.com/projects/ofek-pyapp

## What the wrapper actually wraps

PyApp describes itself as a wrapper for Python applications that bootstrap themselves at runtime. That is the whole product in one line: your Python code is not executed by a Python installation on the user's machine, it is executed by a Python distribution carried inside your own binary.

The README illustrates the split with two recordings, one labelled you build and one labelled user runs. The build side is yours, on a machine with the toolchain and the network. The run side belongs to someone who may have nothing installed, and the first thing that binary does is make a working Python available for itself.

That choice has consequences for how you reason about the artefact. The thing you distribute is an executable, not an archive, so installation is download and run. The Python interpreter is an implementation detail the user never sees. And the cost, which is the cost of every bundling strategy, is size and a first-run step that has to happen somewhere on disk.

The crate is published on crates.io as pyapp, currently at version 0.29.0, edition 2021, described as a runtime installer for Python applications. The implementation language is Rust, and the dependency list shows a binary that manages files, streams and progress bars rather than a library you import.

## The build fetches the distribution; the environment decides how

The most informative part of the repository is the Cargo.toml, and specifically the passthrough environment list under package.metadata.cross.build.env. Those are the variables the build forwards into the compilation step, and their names describe the whole configuration surface.

The PYAPP_DISTRIBUTION_* group controls what gets embedded: PYAPP_DISTRIBUTION_SOURCE, PYAPP_DISTRIBUTION_FORMAT, PYAPP_DISTRIBUTION_PATH, PYAPP_DISTRIBUTION_PATH_PREFIX, PYAPP_DISTRIBUTION_PYTHON_PATH, PYAPP_DISTRIBUTION_SITE_PACKAGES_PATH, PYAPP_DISTRIBUTION_PIP_AVAILABLE, plus PYAPP_DISTRIBUTION_EMBED, PYAPP_DISTRIBUTION_VARIANT and the CPU and GIL variants.

The PYAPP_EXEC_* group is the one that decides what runs on the user's machine: PYAPP_EXEC_CODE, PYAPP_EXEC_MODULE, PYAPP_EXEC_NOTEBOOK, PYAPP_EXEC_SCRIPT and PYAPP_EXEC_SPEC. That is a design decision worth pausing on. One wrapper can launch an application as a module, run a script, execute a notebook, or honour a spec, which means the same binary shape works for tools that were never written to be shipped this way.

The PYAPP_EXPOSE_* group covers the management surface, with PYAPP_EXPOSE_ALL_COMMANDS and PYAPP_EXPOSE_CACHE among the names visible in the list. That maps onto the README feature about optional management commands providing functionality such as self updates: whether those commands exist is a build-time decision.

The build dependencies confirm that the distribution is fetched during compilation. They include reqwest with blocking and rustls-tls, tar, zip, flate2 with the zlib-rs backend, bzip2 and zstd, plus base64, regex, rand and highway. Reading an archive, decompressing four formats and verifying content is exactly what a build step that downloads and embeds a distribution has to do.

## Why the binary is built the way it is

The optimisation profile is four lines long, and each line is a decision about the artefact your users download:

```toml
[profile.release]
panic = "abort"
strip = true
lto = true
codegen-units = 1
```

panic = abort removes the unwinding tables, which shrinks a binary and removes the possibility of catching a panic. strip removes symbols. Link-time optimisation and a single codegen unit trade build time for better code, which is the right trade for a binary you ship to people who cannot rebuild it.

The runtime dependencies are similarly chosen. clap with the derive feature is how the management commands are defined, indicatif with improved unicode is the progress reporting for the bootstrap, directories is how per-platform config and data locations are found, fs4 with the sync feature handles file locking so two concurrent runs do not corrupt the unpacked tree, tempfile provides scratch space, os_pipe moves standard streams, and once_cell handles lazy statics.

The TLS choice is worth noticing: reqwest uses rustls with default features turned off, so there is no OpenSSL dependency. For a binary meant to run on machines you do not control, avoiding a system TLS library is a real portability decision, and it also means the network code still speaks HTTPS to whatever the application itself needs.

Archive support spans tar, zip, bzip2, zstd and zlib, which covers how Python distributions are actually published rather than picking one format and hoping.

## Distribution: crates.io, Hatch and MkDocs

PyApp is distributed through two ecosystems, and the badges in the README make the split explicit.

There is a crates.io badge for the pyapp crate and a repository downloads badge, so the Rust project is both a library dependency and a downloadable binary. The Cargo package metadata carries the repository and documentation URLs, which means the crate page doubles as a project page.

Then there is the Hatch badge. A hatch.toml sits at the top of the repository, which means PyApp participates in the Python build ecosystem rather than living beside it: a project using Hatch can produce a PyApp-wrapped binary through its normal build command, with the wrapper behaving as a build backend step.

That is the integration that matters most for adoption. A team already using Hatch for packaging does not need a new toolchain to try this; the wrapper slots into the step where the artefact is produced.

Documentation is Material for MkDocs hosted on GitHub Pages, with separate workflows for the latest and dev versions, so you can read published docs for the current version and for unreleased changes. The how-to page is described as a detailed example walkthrough, which is where the environment variables get explained in the context of a working example.

Continuous integration is split the same way, with a test workflow, a publish workflow, and the two documentation workflows.

## Licensing and the project metadata mismatch

The README states that PyApp may be used under the terms of any of two licences, each linked to its SPDX page, so you pick the one that suits you. The Cargo package metadata declares the same either-or grant, and the source files carry SPDX headers.

An either-or grant is friendlier than one that demands a choice, since it means a team with a house standard is not forced through legal review to use it.

The mismatch is elsewhere. Repository metadata records no recognised terms, which is what a scanner reports when no licence file sits at the top level, and the top-level entries confirm there is not one. The grant is in the README and in Cargo.toml, so a tool that reads files rather than prose will miss it.

That is a small, fixable inconsistency, worth raising as an issue rather than treating as a blocker. For your own compliance work the authoritative statements are the README and the Cargo metadata, and they agree with each other.

On cadence: the newest tag is v0.29.0 from 2025-10-15, preceded by v0.28.0 in July 2025 and v0.27.0 in March 2025, a steady quarterly rhythm, while the repository itself was pushed to on 2026-08-10. The crate version and the working tree are therefore not in lockstep, so read the project history or the documentation site if you need to know what is newer than the crate you installed.

## Alternatives: wheels, system packages, and installer scripts

The comparison that matters is not another Rust tool. It is whether you need to carry an interpreter at all.

The first alternative is a wheel plus an environment. If your users are developers, this is the normal answer: publish to an index, document the install command, and let the user create an environment. Nothing is bundled, the download is kilobytes rather than tens of megabytes, and the user keeps control of the interpreter version. The cost is exactly the problem PyApp exists to remove, namely that your user now has to set up an environment before your program runs.

The second is a platform package, a deb or an rpm depending on the target. A system package can depend on python3 being present, so it inherits the same prerequisite problem, but it gains integration with the platform updater, which PyApp does not try to match. For an internal tool on machines your organisation manages, this is often the least surprising option.

The third is a shell or PowerShell install script that creates a virtual environment and puts an entry point on the path. It is small, inspectable, and it fails in the most legible way when something is missing, at the cost of asking the user to trust a script from the internet.

PyApp is worth its weight when the user is not a developer, when there is no package manager to hook into, and when the install has to be one file with no prerequisites. If any of those three does not hold, the simpler options above are cheaper and easier to support.

## Where PyApp is the wrong tool

The first boundary is that the build needs the network. The build dependencies include a blocking HTTP client and four archive and compression formats, which is what embedding a distribution costs. On a build machine behind a proxy, or in a hermetic build that forbids downloads, this is a blocker, and there is no vendored path described in the README.

The second is that bundling a Python distribution is a large payload. That cost is paid in download size and in disk space on first run, and it is paid again per platform you build for, since each binary carries its own copy. For a large application on top of the interpreter, the multiplier is uncomfortable.

The third is the wrong shape for some applications. If your users already have Python, or your application is a library rather than a program, a normal distribution channel does the job with less machinery. And if your application needs a native extension compiled against the local machine, a bundled interpreter built elsewhere is the wrong host for it.

The fourth is the management surface. Self-update and related commands are a convenience for the user and a commitment for you: an application that can replace itself needs a channel you keep alive, and an application that exposes all commands exposes more surface than a plain binary does.

The fifth is versioning discipline. Embedding an interpreter means you choose a Python version at build time, and that choice is baked into a binary that users may keep for a long time.

## Conclusion

Adopt PyApp if you are shipping a Python application to users who will not install Python, because the whole problem is solved at the packaging layer rather than by asking users to set up an environment, and the result is one binary per platform with optional self-update. Do not adopt it if your users already have a managed Python environment or if your build machine cannot reach the distribution at compile time, since the distribution is fetched during the build. Verify four things first. Read the environment matrix in the Cargo metadata, because the build passes through a long list of PYAPP_DISTRIBUTION_* and PYAPP_EXEC_* variables and those settings are what decide how your app is embedded and later executed. Decide whether you want management commands at all, since self-update and related commands are optional and exposing them changes what a user can do to your binary. Check the terms before you depend on it, because the README offers a choice of Apache-2.0 or MIT and the Cargo package declares the same dual grant. And confirm your build pipeline can handle the download step, since build dependencies include a blocking HTTP client, archive readers and compression crates, which is a network fetch inside your build. Start by building one application with defaults, then read the how-to walkthrough before tuning any of the passthrough variables.

## FAQ

### What does PyApp do?

It is a Rust wrapper for Python applications that bootstrap themselves at runtime, embedding a Python distribution into a standalone binary during the build so the end user needs nothing installed.

### How do I configure what PyApp embeds and how it executes?

Through the environment variables the build forwards: PYAPP_DISTRIBUTION_* for the embedded distribution and its format, path, variant and pip availability, and PYAPP_EXEC_* to choose running code, a module, a script, a spec or a notebook.

### Can PyApp update itself?

Management commands such as self updates are a listed feature and are optional, configured at build time through the PYAPP_EXPOSE_* settings, with PYAPP_EXPOSE_ALL_COMMANDS among them.

### What licence is PyApp under?

The README offers a choice of Apache-2.0 or MIT, and the Cargo package declares license = "Apache-2.0 OR MIT". Repository metadata records the licence as unknown because no licence file sits at the top level.

### Does PyApp work with Hatch?

Yes. A hatch.toml is included in the repository and the README carries a Hatch project badge, so a Hatch-based project can produce a PyApp binary through its normal build flow.

## Sources

- [Issues](https://github.com/ofek/pyapp/issues)
- [ofek/pyapp on GitHub](https://github.com/ofek/pyapp)
- [Project website](https://ofek.dev/pyapp/)
- [README](https://github.com/ofek/pyapp/blob/master/README.md)
- [Releases](https://github.com/ofek/pyapp/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/ofek-pyapp
