Bundle any uv project into a single runnable .py file.
Project description
zuv
zuv packs your entire uv project into a single .py file so you can hand it to a friend, drop it on a server, or attach it to an email and it just runs. The recipient only needs uv installed — no Python install, no pip install -r, no virtualenv setup, no folder structure to preserve.
uv run my-app.py
One file. One command. Same behaviour on any machine.
Why
Shipping a Python project usually means a zip, a README about setting up a venv, a requirements.txt, and a prayer that the recipient has the right Python version. zuv collapses all of that into one .py file that:
- Carries the whole project inside it (your
src/,pyproject.toml,uv.lock, configs, assets — everything you don't.gitignore). - On first run, extracts itself into
.zuv/<name>_<hash>/next to the script, lets uv build a.venvinside that folder, then runs your entry point. - On every later run, skips the extraction and just executes.
- Stays tiny (under ~10 KB even for a FastAPI app) because dependencies are installed at runtime by uv from PyPI, not embedded.
Install
uv tool install zuv
Project layout
Any standard uv project works. The most common shape:
my-project/
pyproject.toml # [project] dependencies = [...]
src/
main.py # an executable script (with if __name__ == "__main__")
main.py at the project root also works.
Build
From inside the project:
zuv build
# -> ./dist/<project-name>.py
Or point at a project explicitly:
zuv build ./my-project -o ./dist/my-app.py -e src/main.py
zuv build writes a fresh single-file script to the output path (use --clean to wipe the output's parent directory first).
Bundles ship .py sources, not pre-compiled bytecode. On first run the loader compiles .pyc files on the recipient's machine, so the same bundle works across operating systems and across Python minor versions.
Entry point resolution:
--entry/-eflag[tool.zuv].entryinpyproject.tomlsrc/main.pyif it exists, otherwisemain.py
Run
uv run dist/my-app.py
# or, equivalently, via zuv:
zuv run dist/my-app.py -- --any --args --you --like
The shebang + PEP 723 header makes uv run the canonical entrypoint; zuv run is a thin wrapper for users who don't want to remember which tool to invoke. First run: uv extracts the bundle, creates dist/.zuv/<name>_<hash>/.venv, installs deps, runs your entry. Subsequent runs go straight to executing.
Double-click bundles (--zip)
For recipients who don't have uv (or even Python) yet:
zuv build --zip
# -> ./dist/<project-name>.zip
The zip contains three files:
my-app.py the same single-file bundle a non-zip build produces
run.bat Windows launcher: installs uv if missing, then runs my-app.py
run.sh Unix/macOS launcher: same, with curl
The recipient extracts the zip and double-clicks run.bat (Windows) or runs ./run.sh (Unix/macOS). The launcher detects whether uv is on PATH; if not, it runs Astral's official installer (which brings its own Python), prepends ~/.local/bin to PATH, then uv runs the bundled .py. After that, they can run my-app.py directly forever.
The inner .py is byte-for-byte the same single-file bundle you'd get without --zip — the launchers are just a bootstrap convenience for fresh machines.
Offline bundles (--deps)
Ship your app to machines with no internet. zuv embeds the wheels for your locked dependencies so the recipient runs it without ever hitting PyPI.
default build --deps build
+------------------+ +-----------------------+
| app.py ~10 KB | | app.py ~tens of MB |
+------------------+ | + embedded wheels |
| needs internet | +-----------------------+
| to install deps | | runs fully offline |
+------------------+ +-----------------------+
How to use it
Bare --deps bundles for your current OS only (the common case — you build on Windows, you ship to Windows):
zuv build --deps
To target other platforms, pass labels (comma-separated) or all:
zuv build --deps all # every platform listed below
zuv build --deps linux # just Linux x86_64
zuv build --deps windows,linux # Windows + Linux x86_64
zuv build --deps macos-arm,linux-arm # ARM only
Available platform labels
+-------------+----------------------+
| label | target |
+-------------+----------------------+
| windows | Windows x86_64 |
| linux | Linux x86_64 |
| linux-arm | Linux aarch64 |
| macos | macOS Intel |
| macos-arm | macOS Apple Silicon |
+-------------+----------------------+
Bundle size guide
no --deps ~10 KB (recipient needs internet)
--deps (host only) ~few MB (one platform)
--deps all ~tens MB (five platforms)
Notes
- Wheels are tied to the Python minor version you build with. A bundle built on Python 3.14 won't install offline on Python 3.13.
- Sdist-only deps are skipped (
--only-binary=:all:). Those packages still need the network at install time on platforms where no wheel exists. - The build host needs
uvand internet access — the build itself downloads the wheels.
What --deps can't ship: system libraries
Some packages (OpenCV, PySide/PyQt, psycopg2, ...) load OS-level shared libraries at import time — libGL.so.1, libpq.so.5, etc. Wheels can't bundle those; on a bare Linux server the bundle installs fine but errors at first import with ImportError: libGL.so.1: cannot open shared object file. Install the named lib once with your distro's package manager (apt install libgl1, apk add mesa-gl, etc.) and the bundle runs. Prefer -headless variants where they exist (e.g. opencv-python-headless) to avoid the issue entirely.
Try the included examples
zuv build examples/bigtest -o dist/bigtest.py
uv run dist/bigtest.py
zuv build examples/fastapi -o dist/fastapi.py
uv run dist/fastapi.py
Sibling overrides
The bundle's entry script runs with the extracted project folder as its CWD, so anything you would normally find next to your code (config files, frontend bundles, .env files) still works the same way.
Inspect a built file
zuv inspect dist/my-app.py
Prints the entry, build hash, SHA-256, Python cache tag, PEP 723 metadata, and a summary of the embedded loader bytecode. The payload itself is elided so the output stays useful for LLMs and code review.
Clear caches
zuv clean # walk cwd, remove every .zuv/ found
zuv clean dist/ # or scope to a directory
zuv clean dist/app.py # or to a built file (its parent is used)
The runtime also honors $ZUV_CACHE_DIR (and $ZUV_MAX_EXTRACT_BYTES for the decompression-bomb cap, default 2 GiB). If the script's directory isn't writable (system bin, read-only mount), the loader automatically falls back to $XDG_CACHE_HOME/zuv, %LOCALAPPDATA%\zuv, or ~/.cache/zuv.
A small caveat
Don't name your project the same as one of its dependencies. For example, a project named fastapi that depends on fastapi will confuse uv during install. Rename it to fastapi-example (or similar) and you're fine.
How it works
The output .py has five parts:
- A
#!/usr/bin/env -S uv run --scriptshebang and a PEP 723 metadata block declaringrequires-python(no deps — uv reads those from the embeddedpyproject.tomlafter extraction). - Metadata globals:
_ZUV_ENTRY,_ZUV_BUILD_ID,_ZUV_SHA(sha256 of decoded payload),_ZUV_PY_TAG(build-time Python cache tag). _ZUV_PAYLOAD— a deterministictar.xzof your project tree, base85-encoded (uv's script runner requires UTF-8 source, which rules out raw-binary appends)._ZUV_LOADER— the runtime loader,compile()d to bytecode thenmarshal+zlib+base85 (opaque to casual readers; verify withzuv inspect).- A 3-line stub that
execs the loader, which then verifies the payload sha, extracts into.zuv/<stem>_<hash>/with a.zuv-readysentinel, and runsuv run --project <extracted> <entry>.
Dependencies aren't bundled inside the .py. uv installs them into the extracted project's local .venv on first run, so binary wheels work natively and the bundle stays small.
Layout
src/
pyproject.toml
zuv/
cli.py # zuv CLI (build, inspect)
builder.py # tarball + base85 + compile loader + emit .py
inspector.py # zuv inspect
_loader_template.py # runtime loader embedded in every output
constants.py
examples/
bigtest/ # rich + pydantic smoke test
fastapi/ # FastAPI + uvicorn web app
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file zuv-0.2.0.tar.gz.
File metadata
- Download URL: zuv-0.2.0.tar.gz
- Upload date:
- Size: 16.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fdcb1ae148b4ce9a67e3cc3bee365da520cc5d19703969588840c47cf498677d
|
|
| MD5 |
998e20645d194d319e15ac45f791aacb
|
|
| BLAKE2b-256 |
71349e413381d150352538e2a6f9b96bc3934f4a7fcef99bde81001b1e6c1e90
|
Provenance
The following attestation bundles were made for zuv-0.2.0.tar.gz:
Publisher:
pypi.yml on HamzaYslmn/zuv
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
zuv-0.2.0.tar.gz -
Subject digest:
fdcb1ae148b4ce9a67e3cc3bee365da520cc5d19703969588840c47cf498677d - Sigstore transparency entry: 1561802354
- Sigstore integration time:
-
Permalink:
HamzaYslmn/zuv@12730ab63b57730ac568a77e7644173733bbd2da -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/HamzaYslmn
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
pypi.yml@12730ab63b57730ac568a77e7644173733bbd2da -
Trigger Event:
push
-
Statement type:
File details
Details for the file zuv-0.2.0-py3-none-any.whl.
File metadata
- Download URL: zuv-0.2.0-py3-none-any.whl
- Upload date:
- Size: 21.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
546bbcfb8e020615d9aa511ff852e558ae38c2ef1a47e58a9e3ee54d7f2d5c23
|
|
| MD5 |
e1a2c68ce3e09a022ed0aa45df27279e
|
|
| BLAKE2b-256 |
269200de64f54ba0894695077f4161b418b859e5de325830e4be8bffc1f79649
|
Provenance
The following attestation bundles were made for zuv-0.2.0-py3-none-any.whl:
Publisher:
pypi.yml on HamzaYslmn/zuv
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
zuv-0.2.0-py3-none-any.whl -
Subject digest:
546bbcfb8e020615d9aa511ff852e558ae38c2ef1a47e58a9e3ee54d7f2d5c23 - Sigstore transparency entry: 1561802499
- Sigstore integration time:
-
Permalink:
HamzaYslmn/zuv@12730ab63b57730ac568a77e7644173733bbd2da -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/HamzaYslmn
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
pypi.yml@12730ab63b57730ac568a77e7644173733bbd2da -
Trigger Event:
push
-
Statement type: