The zipapp module packages a Python application into one executable ZIP archive, commonly using the .pyz extension. Python adds the archive to sys.path and runs its __main__.py. This is useful for internal tools, automation scripts, command-line utilities, prototypes, and distribution to systems that already have a compatible Python runtime.
A zipapp is not a standalone native executable. It does not automatically include Python, solve every dependency, or make compiled extensions importable from inside a ZIP file. For public desktop distribution or machines without Python, a full application packager may be more appropriate.
Minimal structure
An application archive needs a __main__.py file at its root.
my_app/
├── __main__.py
└── package/
├── __init__.py
└── cli.py
The entry file starts the application.
from package.cli import main
if __name__ == "__main__":
raise SystemExit(main())
This keeps main() independently testable and propagates an exit code.
Create an archive from the command line
python -m zipapp my_app -o my_app.pyz
Run it with:
python my_app.pyz --help
Python treats the archive as an importable directory and executes its entry point.
Create an archive with the API
zipapp.create_archive() automates builds from Python code.
from zipapp import create_archive
create_archive(
"my_app",
target="dist/my_app.pyz",
)
Create the destination directory first and use pathlib to organize the build pipeline.
Generate an entry point
When the source tree has no __main__.py, specify a main function as package.module:function.
python -m zipapp my_app \
-m "package.cli:main" \
-o my_app.pyz
The tool generates a small __main__.py that imports and calls the function.
Main function design
The entry function should be importable and callable without positional arguments unless it deliberately reads sys.argv.
def main():
args = parser.parse_args()
run(args)
return 0
Do not execute the entire program at module import time. Imports should establish definitions, not perform the command.
Shebang and interpreter
The --python option or interpreter parameter adds an interpreter line to the archive.
python -m zipapp my_app \
--python "/usr/bin/env python3" \
-o my_app.pyz
chmod +x my_app.pyz
./my_app.pyz
This works on systems that support shebang execution. On Windows, run python my_app.pyz or configure an association.
Runtime version checks
A generic python3 shebang can resolve to different releases. Validate minimum requirements at startup.
import sys
if sys.version_info < (3, 12):
raise SystemExit("Python 3.12 or newer is required")
Document the versions tested by the project.
Compression
The --compress option or compressed=True compresses entries.
create_archive(
"my_app",
target="dist/my_app.pyz",
compressed=True,
)
Compression reduces file size but adds CPU work during build and reading. Small applications may see little benefit.
Rebuilding from clean sources
The API can process existing archives in selected workflows, but it should not be treated as a generic ZIP editor.
Rebuild from a clean source tree to improve traceability and reproducibility.
Pure Python dependencies
Packages written entirely in Python can be installed into the staging directory before the build.
python -m pip install \
--target build/app \
--requirement requirements.txt
cp -r src/my_package build/app/
python -m zipapp build/app -o dist/app.pyz
Use an isolated build environment and pinned versions.
Native extensions
Compiled modules such as .so and .pyd files generally cannot be imported directly from inside a ZIP because the operating-system loader needs a real file.
Keep native dependencies installed outside the archive, extract them through a controlled process, or choose another packaging tool.
Detect native dependencies
Do not rely only on a package's name. Inspect wheels and test in a clean environment.
A transitive dependency can introduce native code even when the top-level package appears pure.
Package resources
Code running from a ZIP should not assume __file__ names a normal filesystem file.
Use importlib.resources to read templates and packaged data.
from importlib.resources import files
text = (
files("my_package")
.joinpath("data/default.json")
.read_text(encoding="utf-8")
)
Resources that require a real path
Some libraries require an actual pathname. importlib.resources.as_file() can temporarily materialize a resource where supported.
Use it as a context manager and never keep the path after exit.
Writable files
Treat archive contents as read-only. Do not write configuration, caches, or databases beside internal modules.
Use user-data, cache, or temporary directories and allow explicit configuration.
Current working directory
Do not assume the process starts in the archive's directory. Path.cwd() depends on where the user launched the command.
Use configured absolute paths or packaged resources.
Imports
Organize the code as a package and prefer absolute imports. Fragile relative imports and modules named after standard-library packages can cause conflicts.
Test the final artifact, not only the source tree.
Namespace packages
Namespace packages can work, but combinations of internal and external portions require careful testing.
A normal package is usually more predictable for a compact tool.
sys.path behavior
During execution, the archive appears on the import path. Its position can interact with externally installed packages.
Use unique package names and never depend on accidental shadowing.
External dependency conflicts
If a dependency is not bundled, the application may import an arbitrary installed version from the user's environment.
Validate versions or distribute instructions for a dedicated virtual environment.
Use with a virtual environment
An internal deployment can create a venv containing Python and native dependencies, then execute the .pyz with that interpreter.
The archive contains application code while the environment provides runtime libraries.
zipapp versus pipx
Tools such as pipx install command-line applications into isolated environments. A conventional package with an entry point can be easier to update than a manually distributed archive.
Choose zipapp when a single file materially simplifies operations.
Version metadata
Expose application version through --version and optionally include a manifest with commit, build time, and dependency lock information.
__version__ = "1.4.0"
Reproducible builds
ZIP entries contain timestamps and ordering information, which can produce different hashes. Control timestamps, sort inputs, and pin dependencies for reproducibility.
Basic zipapp usage may need an additional normalization step for strict deterministic builds.
File filters
The API accepts a filter that decides which paths are included.
def include(path):
parts = set(path.parts)
return not parts.intersection({"__pycache__", ".git", "tests"})
create_archive("build/app", "dist/app.pyz", filter=include)
Do not accidentally exclude required resources. Run the final archive in a clean directory.
Do not embed secrets
A zipapp is an ordinary ZIP and can be opened easily. Never include passwords, API tokens, private keys, or credentials.
Load secrets from a manager, protected environment, or external configuration.
Checksums and signatures
Distribute a hash or signature to verify integrity. Python does not automatically validate a zipapp signature before execution.
The deployment process must verify the file before starting the interpreter.
Untrusted archives
Running a .pyz executes Python code with the user's permissions. Never download and execute unknown archives.
Use HTTPS, signatures, trusted origins, and least privilege.
Atomic updates
A single file is easy to replace, but update atomically. Download under a temporary name, validate, and call os.replace().
Do not overwrite the active file before verification completes.
Rollback
Keep the previous version until a smoke test passes. A launcher or symlink can select the active artifact.
Do not combine irreversible data migrations with an update that has no rollback plan.
Containers
A zipapp can reduce application files inside a container image, but it still needs Python and dependencies.
Containers already provide layers and isolation, so measure whether the archive adds operational value.
Arguments and exit codes
Use argparse and consistent exit statuses. raise SystemExit(main()) propagates the return code.
Write normal output to stdout and errors to stderr.
Logging
Do not write logs inside the archive. Configure stderr, an external file, or structured logging.
Include archive version at process startup for diagnostics.
Testing
Build the archive in CI and execute real commands in a clean environment. Test help, version, missing dependencies, resources, exit codes, paths with spaces, Windows, and POSIX.
Compare source-tree and .pyz behavior.
Common mistakes
Common failures include omitting __main__.py, bundling native extensions inside the ZIP, treating __file__ as a normal path, writing into the archive, relying on an unknown external dependency version, embedding secrets, skipping artifact tests, and confusing zipapp with a self-contained executable.
Conclusion
zipapp packages pure Python applications into a single archive executed by Python. Use a clean entry point, importlib.resources, pinned dependencies, read-only archive assumptions, and end-to-end artifact tests.
Consult the official zipapp documentation and Python sysconfig for details about the destination runtime.







