Python zipapp: Build Executable .pyz Files

Published on: August 13, 2026
Reading time: 5 minutes
Organized binders representing applications packaged as executable .pyz files with Python zipapp

Python zipapp creates executable ZIP archives containing a Python application. These files commonly use the .pyz extension and can be launched with python application.pyz. On POSIX systems, they may also contain a shebang and executable permission so they behave like ordinary commands.

The format works well for internal utilities, command-line tools, administrative scripts, and pure-Python applications that benefit from single-file delivery. It does not compile the project into a native binary and it does not include the Python interpreter. The target machine still needs a compatible Python installation, and bundled dependencies must support execution from a ZIP archive.

How a .pyz application works

A Python Zip Application is a regular ZIP file with a root-level __main__.py. When executed, Python places the archive on sys.path and runs that module as the entry point.

my_app/
├── __main__.py
├── commands.py
└── data/
    └── config.json

Create the archive with:

python -m zipapp my_app

Then run it:

python my_app.pyz

Imports work through Python’s normal ZIP import machinery. For packaged data, use the patterns described in Python importlib.resources.

Generate an entry point

If the source directory does not contain __main__.py, the -m option can generate one that imports and calls a no-argument function.

python -m zipapp my_app \
  -m "my_app.cli:main" \
  -o tool.pyz

The callable must be included in the archive:

# my_app/cli.py
def main():
    print('Application started')

The required format is package.module:callable. Do not use -m when the source already has __main__.py.

Add a shebang

The -p option prepends an interpreter line. On POSIX, zipapp also sets the executable bit.

python -m zipapp my_app \
  -p "/usr/bin/env python3" \
  -o tool.pyz

./tool.pyz

The selected interpreter must be portable for the intended audience. /usr/bin/env python3 is often more flexible than an absolute path, but it still assumes that a compatible python3 command exists.

Compression trade-offs

Files are stored uncompressed by default. Use --compress to enable deflate:

python -m zipapp my_app --compress -o tool.pyz

Compression lowers distribution size but may increase build and startup work. Source code usually compresses well; JPEG, PNG, and other already-compressed formats may barely shrink.

Automate with create_archive()

The Python API provides the same functionality.

import zipapp

zipapp.create_archive(
    source='my_app',
    target='dist/tool.pyz',
    interpreter='/usr/bin/env python3',
    main='my_app.cli:main',
    compressed=True,
)

source can be a directory, an existing archive, or a binary input stream. target can be a path or binary output stream. The caller must close any streams it supplies.

Filter build content

The filter callback receives each relative Path and decides whether it enters the archive.

from pathlib import Path
import zipapp

IGNORED = {'__pycache__', '.git', '.pytest_cache'}

def include(path: Path) -> bool:
    if any(part in IGNORED for part in path.parts):
        return False
    return path.suffix not in {'.pyc', '.log', '.env'}

zipapp.create_archive(
    'my_app',
    'dist/my_app.pyz',
    filter=include,
    compressed=True,
)

Always exclude secrets, local environment files, logs, caches, and development artifacts. Build from a clean staging directory and inspect the final ZIP contents.

Bundle pure-Python dependencies

Install dependencies into the build tree before packaging:

python -m pip install \
  --requirement requirements.txt \
  --target build/my_app

python -m zipapp build/my_app \
  -m "my_app.cli:main" \
  -o dist/my_app.pyz

Pin versions and hashes for reproducible builds. Do not install dependencies directly into the source tree; use a disposable build directory.

C extensions cannot load from inside the ZIP

Native modules such as .so and .pyd files generally cannot be loaded directly from an archive because the operating-system loader requires real filesystem objects.

Packages such as NumPy, cryptographic libraries, database drivers, and image processors may include native components. Require them externally, ship compatible binaries next to the archive, or use another packaging method. Remember architecture and operating-system compatibility.

Read packaged resources correctly

Do not assume that __file__ points to a normal directory. A resource inside the archive may not exist as a persistent path.

from importlib.resources import files

text = (
    files('my_app.data')
    .joinpath('config.json')
    .read_text(encoding='utf-8')
)

If an API requires a filesystem path, use importlib.resources.as_file() within a context manager. The full workflow is covered in Python importlib.resources.

Inspect the embedded interpreter

zipapp.get_interpreter() reads the shebang.

import zipapp

interpreter = zipapp.get_interpreter('dist/my_app.pyz')
print(interpreter)

The CLI equivalent is python -m zipapp archive.pyz --info. Use this in release checks to confirm the expected launcher.

Copy and modify an existing archive

create_archive() can copy an existing .pyz and replace its interpreter line.

zipapp.create_archive(
    'old.pyz',
    'new.pyz',
    interpreter='/usr/bin/env python3',
)

The input and output path cannot be identical. Write to a new file, validate it, synchronize if necessary, and then replace the old artifact atomically.

Avoid unsafe in-place overwrites

The documentation demonstrates modifying an archive through BytesIO, but warns that an error during overwrite may destroy the original. A safer production pattern uses a temporary sibling file and os.replace().

import os
import zipapp

new_path = 'app.pyz.new'
zipapp.create_archive('app.pyz', new_path, '/usr/bin/env python3')
validate(new_path)
os.replace(new_path, 'app.pyz')

Reproducible artifacts

A repeatable build requires controlled Python versions, dependencies, source files, permissions, timestamps, and archive order. Record the final checksum.

import hashlib
from pathlib import Path

data = Path('dist/app.pyz').read_bytes()
print(hashlib.sha256(data).hexdigest())

Python compileall and py_compile can verify syntax, but precompiled bytecode does not remove interpreter-version constraints.

Distribution security

A .pyz is executable code. Sign it or publish cryptographic hashes through a trusted channel, restrict release permissions, and never execute unknown archives.

ZIP packaging does not hide content. Anyone can inspect the archive. Do not embed API keys, passwords, private certificates, or production configuration.

Python version compatibility

The archive must match the interpreter used at runtime. New syntax, recent APIs, and dependency requirements can fail on older installations. Declare a minimum supported version and test a matrix of clean environments.

import sys

if sys.version_info < (3, 11):
    raise SystemExit('Python 3.11 or newer is required')

A shebang cannot express “version X.Y or later.” It points to a command; runtime checks remain useful.

When zipapp is a strong choice

Use zipapp when the application is mostly pure Python, users already have a compatible interpreter, and one-file delivery simplifies operations. Internal automation and portable command-line utilities are excellent candidates.

Avoid it when you must bundle Python itself, depend heavily on native extensions, require persistent filesystem paths for resources, or need a native installer. Wheels, containers, or executable bundlers may fit better.

Test the generated archive

Do not test only the source directory. Execute the final artifact in a clean environment:

python dist/my_app.pyz --version
python dist/my_app.pyz diagnose

Check that no global packages are required. Test encoding, resources, command-line arguments, error handling, exit codes, and supported Python versions. Documentation generated with Python pydoc can help verify public APIs.

Common mistakes

  • Missing both __main__.py and the main option.
  • Packaging secrets and development files.
  • Assuming native extensions run inside the ZIP.
  • Using __file__ for every resource.
  • Selecting a non-portable shebang.
  • Overwriting the original without an atomic replacement.
  • Testing only on the build machine.

Best practices

  • Build in a clean disposable directory.
  • Pin dependencies and record hashes.
  • Filter caches, logs, and secrets.
  • Use importlib.resources for data.
  • Test the actual .pyz in clean environments.
  • Document the minimum Python version.
  • Distribute through a trusted channel.

Conclusion

Python zipapp turns a source tree into a transparent, executable single-file application. Entry points, shebangs, filtering, and pure-Python dependency bundling make it practical for many internal tools.

It does not bundle the interpreter and it does not solve native binary dependencies. Plan compatibility, resources, security, and artifact testing. Consult the official zipapp documentation and the zipimport documentation.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    Developer configuring a Python HTTPS server and TLS certificate
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    HTTPSServer: Build a Local HTTPS Server in Python

    Learn Python HTTPSServer for local HTTPS services, TLS certificates, threaded handling, testing, and practical security limits.

    Ler mais

    Tempo de leitura: 4 minutos
    08/10/2026
    Protected files representing secure TAR extraction with Python
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    tarfile extraction_filter: Extract TAR Safely

    Learn Python tarfile extraction_filter for safer TAR extraction, path validation, links, permissions, and resource limits.

    Ler mais

    Tempo de leitura: 5 minutos
    08/10/2026
    Python code showing warnings controlled with catch_warnings
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    catch_warnings: Capture Python Warnings in Tests

    Learn Python catch_warnings to capture, test, and control warnings with focused filters and safe temporary scopes.

    Ler mais

    Tempo de leitura: 5 minutos
    07/10/2026
    Python code representing persistent pickle references
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    pickle persistent_id: Serialize External References

    Learn Python pickle persistent_id for stable external references, validation, security, performance, and long-term compatibility.

    Ler mais

    Tempo de leitura: 5 minutos
    07/10/2026
    Binary code representing Python buffers and memory views
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    memoryview.count: Count Values Without Buffer Copies

    Learn Python memoryview.count to count bytes and values in buffers without copies, with formats, limits, and practical safety.

    Ler mais

    Tempo de leitura: 5 minutos
    06/10/2026
    Python code and type annotations
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    annotationlib: Avoid Circular Imports in Annotations

    Learn Python annotationlib to inspect deferred annotations, avoid circular imports, and build safer runtime tooling.

    Ler mais

    Tempo de leitura: 5 minutos
    06/10/2026