Python’s zipapp module creates executable .pyz archives — zip files with a #!/usr/bin/env python3 shebang that can be run directly as python app.pyz or ./app.pyz. import zipapp. Create: zipapp.create_archive(source, target=None, interpreter="/usr/bin/env python3", main=None, filter=None, compressed=False) — source is a directory with a __main__.py or an existing zip; target defaults to source with .pyz extension; interpreter is the shebang (None = no shebang); main overrides the entry point as "pkg.module:function" (auto-generates __main__.py); filter(path) → bool selects which files to include; compressed=True uses ZIP_DEFLATED. Read shebang: zipapp.get_interpreter(archive) → str | None. The resulting .pyz file is a valid zip: zipfile.ZipFile("app.pyz") works normally. On POSIX, set execute bit (chmod +x) and run as ./app.pyz; on Windows, associate .pyz with python. Claude Code generates single-file CLI tools, deployment bundles, Lambda layers, and self-extracting archives.
CLAUDE.md for zipapp
## zipapp Stack
- Stdlib: import zipapp, zipfile, shutil
- Create: zipapp.create_archive("src/", target="dist/app.pyz",
- interpreter="/usr/bin/env python3")
- Entry: zipapp.create_archive("src/", main="myapp.cli:main")
- # auto-generates __main__.py calling myapp.cli:main()
- Compress: zipapp.create_archive("src/", compressed=True)
- Read: zipapp.get_interpreter("app.pyz")
- Run: python app.pyz OR ./app.pyz (after chmod +x)
- Note: stdlib-only; for third-party deps use shiv or pex
zipapp Executable Archive Pipeline
# app/zipapputil.py — builder, inspector, bundler, deployer
from __future__ import annotations
import os
import shutil
import stat
import zipapp
import zipfile
from dataclasses import dataclass, field
from pathlib import Path
# ─────────────────────────────────────────────────────────────────────────────
# 1. Archive inspection
# ─────────────────────────────────────────────────────────────────────────────
@dataclass
class PyzInfo:
"""
Metadata about an existing .pyz archive.
Example:
info = inspect_pyz("dist/app.pyz")
print(info)
"""
path: Path
interpreter: "str | None"
size_bytes: int
files: list[str]
has_main: bool
compressed: bool
def __str__(self) -> str:
interp = self.interpreter or "(no shebang)"
return (
f"PyzInfo({self.path.name} {self.size_bytes} bytes "
f"interp={interp!r} files={len(self.files)} "
f"has_main={self.has_main} compressed={self.compressed})"
)
def inspect_pyz(path: "str | Path") -> PyzInfo:
"""
Return metadata for a .pyz archive.
Example:
info = inspect_pyz("dist/app.pyz")
print(info)
for f in info.files:
print(f" {f}")
"""
p = Path(path)
interpreter = zipapp.get_interpreter(str(p))
with zipfile.ZipFile(p) as zf:
names = zf.namelist()
infos = zf.infolist()
compressed = any(
zi.compress_type == zipfile.ZIP_DEFLATED for zi in infos
)
return PyzInfo(
path=p,
interpreter=interpreter,
size_bytes=p.stat().st_size,
files=names,
has_main="__main__.py" in names,
compressed=compressed,
)
def read_pyz_file(archive: "str | Path", inner_path: str) -> bytes:
"""
Read a file from inside a .pyz archive.
Example:
src = read_pyz_file("dist/app.pyz", "__main__.py")
print(src.decode())
"""
with zipfile.ZipFile(str(archive)) as zf:
return zf.read(inner_path)
# ─────────────────────────────────────────────────────────────────────────────
# 2. Builder helpers
# ─────────────────────────────────────────────────────────────────────────────
def build_pyz(
source: "str | Path",
target: "str | Path | None" = None,
main: "str | None" = None,
interpreter: str = "/usr/bin/env python3",
compressed: bool = True,
executable: bool = True,
filter_fn=None,
) -> Path:
"""
Build a .pyz from source directory. Returns the output path.
Args:
source: Directory containing __main__.py (or use main= to generate one)
target: Output .pyz path (default: source.pyz)
main: Entry point like "myapp.cli:main" (generates __main__.py)
interpreter: Shebang line (None = no shebang)
compressed: Deflate the archive contents
executable: chmod +x the output on POSIX
filter_fn: (path: Path) -> bool; return False to exclude
Example:
path = build_pyz("src/myapp", target="dist/myapp.pyz",
main="myapp.cli:main", compressed=True)
"""
src = Path(source)
dest = Path(target) if target else src.parent / (src.name + ".pyz")
dest.parent.mkdir(parents=True, exist_ok=True)
py_filter = None
if filter_fn is not None:
def py_filter(p: Path) -> bool:
return filter_fn(p)
zipapp.create_archive(
str(src),
target=str(dest),
interpreter=interpreter,
main=main,
filter=py_filter,
compressed=compressed,
)
if executable and os.name == "posix":
current = dest.stat().st_mode
dest.chmod(current | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
return dest
def exclude_tests_and_cache(path: Path) -> bool:
"""
Filter function for build_pyz: exclude test files and __pycache__.
Example:
pyz = build_pyz("src/app", filter_fn=exclude_tests_and_cache)
"""
parts = path.parts
excluded_dirs = {"__pycache__", "tests", "test", ".git"}
if any(p in excluded_dirs for p in parts):
return False
if path.name.startswith("test_") or path.name.endswith("_test.py"):
return False
return True
# ─────────────────────────────────────────────────────────────────────────────
# 3. Staged bundle builder
# ─────────────────────────────────────────────────────────────────────────────
class PyzBundler:
"""
Build a .pyz by explicitly collecting files from multiple sources.
Example:
bundler = PyzBundler()
bundler.add_directory("src/myapp", prefix="myapp")
bundler.add_file("data/schema.json", arcname="myapp/schema.json")
bundler.set_main("myapp.server:main")
pyz = bundler.build("dist/server.pyz")
"""
def __init__(self) -> None:
self._files: list[tuple[Path, str]] = [] # (real_path, arcname)
self._main: "str | None" = None
self._interpreter: str = "/usr/bin/env python3"
self._compressed: bool = True
def add_directory(
self,
directory: "str | Path",
prefix: str = "",
filter_fn=None,
) -> "PyzBundler":
"""Add all .py files from directory under the given prefix."""
root = Path(directory)
for py in sorted(root.rglob("*.py")):
rel = py.relative_to(root)
arcname = str(Path(prefix) / rel) if prefix else str(rel)
if filter_fn is None or filter_fn(py):
self._files.append((py, arcname))
return self
def add_file(
self,
path: "str | Path",
arcname: "str | None" = None,
) -> "PyzBundler":
"""Add a single file at the given arcname."""
p = Path(path)
self._files.append((p, arcname or p.name))
return self
def set_main(self, main: str) -> "PyzBundler":
"""Set entry point as 'pkg.module:function'."""
self._main = main
return self
def set_interpreter(self, interpreter: str) -> "PyzBundler":
self._interpreter = interpreter
return self
def build(self, target: "str | Path", tmp_dir: "str | Path | None" = None) -> Path:
"""
Assemble and write the .pyz file.
Returns the path to the created archive.
"""
import tempfile
ctx = tempfile.TemporaryDirectory() if tmp_dir is None else None
work = Path(tmp_dir) if tmp_dir else Path(ctx.name) # type: ignore[union-attr]
try:
stage = work / "_stage"
stage.mkdir(parents=True)
for real_path, arcname in self._files:
dest = stage / arcname
dest.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(str(real_path), str(dest))
if self._main:
main_py = stage / "__main__.py"
pkg, _, func = self._main.rpartition(":")
main_py.write_text(
f"import sys\nimport {pkg}\n"
f"sys.exit({pkg}.{func}())\n"
)
dest_pyz = Path(target)
zipapp.create_archive(
str(stage),
target=str(dest_pyz),
interpreter=self._interpreter,
compressed=self._compressed,
)
return dest_pyz
finally:
if ctx is not None:
ctx.cleanup()
# ─────────────────────────────────────────────────────────────────────────────
# 4. Repack with different settings
# ─────────────────────────────────────────────────────────────────────────────
def repack_pyz(
source_pyz: "str | Path",
target: "str | Path",
interpreter: "str | None" = None,
compressed: bool = True,
) -> Path:
"""
Repack an existing .pyz with a new interpreter or compression setting.
Useful for cross-platform redistribution (strip shebang for Windows).
Example:
repack_pyz("dist/app.pyz", "dist/app-win.pyz", interpreter=None)
"""
import tempfile
src = Path(source_pyz)
dest = Path(target)
with tempfile.TemporaryDirectory() as td:
stage = Path(td) / "stage"
stage.mkdir()
with zipfile.ZipFile(src) as zf:
zf.extractall(stage)
effective_interp = (
interpreter
if interpreter is not None
else zipapp.get_interpreter(str(src))
)
zipapp.create_archive(
str(stage),
target=str(dest),
interpreter=effective_interp,
compressed=compressed,
)
return dest
# ─────────────────────────────────────────────────────────────────────────────
# Demo
# ─────────────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
import subprocess
import tempfile
print("=== zipapp demo ===")
with tempfile.TemporaryDirectory() as td:
td_path = Path(td)
# ── create a minimal app directory ────────────────────────────────────
app_dir = td_path / "myapp"
app_dir.mkdir()
(app_dir / "__main__.py").write_text(
"import sys\n"
"from myapp import greet\n"
"name = sys.argv[1] if len(sys.argv) > 1 else 'World'\n"
"print(greet(name))\n"
)
pkg = app_dir / "myapp"
pkg.mkdir()
(pkg / "__init__.py").write_text(
"def greet(name: str) -> str:\n"
" return f'Hello from pyz, {name}!'\n"
)
# ── build_pyz ─────────────────────────────────────────────────────────
print("\n--- build_pyz ---")
pyz = build_pyz(app_dir, target=td_path / "myapp.pyz", compressed=True)
print(f" built: {pyz.name} size={pyz.stat().st_size} bytes")
# ── inspect_pyz ───────────────────────────────────────────────────────
print("\n--- inspect_pyz ---")
info = inspect_pyz(pyz)
print(f" {info}")
for f in info.files:
print(f" {f}")
# ── read_pyz_file ─────────────────────────────────────────────────────
print("\n--- read_pyz_file ---")
main_src = read_pyz_file(pyz, "__main__.py")
print(f" __main__.py:\n {main_src.decode().strip()[:80]!r}")
# ── run the pyz ───────────────────────────────────────────────────────
print("\n--- run pyz ---")
proc = subprocess.run(
[sys.executable, str(pyz), "Claude"],
capture_output=True, text=True
)
print(f" stdout: {proc.stdout.strip()!r}")
# ── PyzBundler ────────────────────────────────────────────────────────
print("\n--- PyzBundler ---")
bundler = PyzBundler()
bundler.add_directory(pkg, prefix="myapp")
bundler.set_main("myapp:greet")
bundler.set_interpreter("/usr/bin/env python3")
bundler2 = td_path / "bundled.pyz"
bundler.build(bundler2)
info2 = inspect_pyz(bundler2)
print(f" {info2}")
# ── repack_pyz (strip shebang) ────────────────────────────────────────
print("\n--- repack_pyz ---")
repacked = repack_pyz(pyz, td_path / "myapp-nointerp.pyz", interpreter=None)
info3 = inspect_pyz(repacked)
print(f" interpreter: {info3.interpreter!r}")
print(f" size: {info3.size_bytes} bytes")
print("\n=== done ===")
For the shiv / pex (PyPI) alternative — shiv and pex embed a virtualenv with third-party packages inside a zip so the bundle is fully self-contained — use shiv or pex when your application depends on packages not in the stdlib (e.g., requests, fastapi); use zipapp for stdlib-only tools where a single-file distribution is needed without the overhead of bundling a virtualenv. For the PyInstaller / Nuitka alternative — PyInstaller and Nuitka compile Python to a native executable with the interpreter embedded — use these when you need a true binary with no Python runtime requirement; use zipapp when the deployment target has Python installed and you want a simple, transparent, inspect-able zip that zipfile.ZipFile can open. The Claude Skills 360 bundle includes zipapp skill sets covering PyzInfo + inspect_pyz() archive inspector, read_pyz_file() content reader, build_pyz() with executable/compressed/filter_fn options, exclude_tests_and_cache() standard filter, PyzBundler with add_directory()/add_file()/set_main()/build() staged assembly, and repack_pyz() for interpreter/compression reconfiguration. Start with the free tier to try executable archive patterns and zipapp pipeline code generation.