Python zipapp Review


Maybe you've done this?

  • I have script to share
  • It has classes and functions that I've neatly componentized
  • I just want to give someone a single file to run
  • I copy/paste all the components into the file and give it to my colleague
  • Rinse, repeat everytime I make a change

Python's zipapp module can really help simplify this process. What follows is the blogification of a talk I gave at work.

Trivial Example

  • Some python (I'm saving the file as – ugly but necessary for now):

    def main():
        print("Hello, world!")
    if __name__ == "__main__":
  • That can be copied to your friend, then as long as they have a recent version of Python (3!):

    Hello, world!

Trivial… and weird

  • Yep, trivial, but I can also put it in a zipfile:

    adding: (deflated 13%)
    • Run it!

      Hello, world!

Trivial… and weirder

  • What's in the file?

    unzip -l
      Length      Date    Time    Name
    ---------  ---------- -----   ----
           78  2023-05-31 22:46
    ---------                     -------
           78                     1 file
  • Now this is fun:

    echo  "#!/usr/bin/env python3" | \
        cat - >
    chmod 0755
    • And run it!

      Hello, world!

Python now has a module to help: zipapp

  • Build it with zipapp (new in Python 3.5):

    python3 -m zipapp trivial/ -o /tmp/hello-world-zipapp \
            -p "/usr/bin/env python3"
    • I'm setting the shebang line – Linux and (maybe) Mac specific
      • You can still run it by typing: python3 hello-world if you want to avoid setting it or if the setting is incorrect for your environment
  • The program is ready to run:

    python3 /tmp/hello-world-zipapp
    Hello, world!

But I have a local module I want to import

  • How about pulling the launch schedule from Spaceflight Now?

    curl -s | grep '<title>' | head -3
    <title>Spaceflight Now</title>
            <title>Private astronauts splash down to close out 9-day commercial research mission</title>
            <title>Live coverage: SpaceX launches more Starlink satellites from California</title>
  • Here's a module to retrieve that RSS feed and pull out the interesting bits - I'll save it as

    import xml.etree.ElementTree as ET
    import requests
    class RetrieveRSS:
        def __init__(self, feed):
            self.feed = feed
            self.results = list()
        def get(self, raw_rss=None):
            raw_rss = raw_rss or requests.get(self.feed).text
            root = ET.fromstring(raw_rss)
            rss_links = dict()
            for channel in root.findall("channel"):
                channel_title = channel.find("title").text
                rss_links[channel_title] = list()
                for channel_item in channel.findall("item"):
            return rss_links
  • And we need a main (and some Python path magic) saved as

    import os
    import sys
    from rss import RetrieveRSS
    def main():
        feed_url = sys.argv[1]
        feed = RetrieveRSS(feed_url)
        rss_links = feed.get()
        for channel in rss_links:
            for link in rss_links[channel]:
                print(f"Title: {link[0]}")
                print(f"   Link: {link[1]}")
  • We can build a package with (notice "-m" option):

    python3 -m zipapp with_modules/ -o /tmp/get-feeds \
            -p "/usr/bin/env python3" \
            -m "get_rss_links:main"
  • Does it work?

    /tmp/get-feeds '' | head -6
    Title: Private astronauts splash down to close out 9-day commercial research mission
    Title: Live coverage: SpaceX launches more Starlink satellites from California
    Title: Live coverage: U.S.-Saudi commercial astronaut crew returns to Earth
  • What does the zip file look like?

    unzip -l /tmp/get-feeds
    Archive:  /tmp/get-feeds
      Length      Date    Time    Name
    ---------  ---------- -----   ----
          368  2023-05-31 22:48
          695  2023-05-31 22:47
           66  2023-05-31 22:48
    ---------                     -------
         1129                     3 files
  • The zipapp module creates

    unzip -p /tmp/get-feeds 
    # -*- coding: utf-8 -*-
    import get_rss_links

Importing from PyPI

  • Just like the previous example, but you can bring in a module from PyPI. Here's the requirements.txt:

  • Main program,

    import sys
    import cowsay
    def main():
  • Build the package in a Python virtual environment:

    python3 -m venv venv
    python3 -m pip install --upgrade -r requirements.txt \
            --target venv
    cp venv/
    python3 -m zipapp venv -p "/usr/bin/env python3" \
            -o /tmp/sayit -m "sayit:main"
  • Test:

    /tmp/sayit "This works on `date`!"
    | This works on Wed May 31 22:48:56 PDT 2023! |
                                                  (__)\       )\/\
                                                      ||----w |
                                                      ||     ||

Support python2 and python3 in one package

I have come code that has to run under Python 2 and 3.

  • I have code for each version of Python:

    ls -1 sender/
  • Here's what the main code looks like:

    import os
    import sys
    if __name__ == "__main__":
        if sys.version_info[0] == 2:
            from main2 import run
            from main3 import run

The Gotchas

  • Pure Python only packaging
  • Favorite modules: numpy, pandas, scikit-learn
    • Have C-modules, which are not transportable (or at least I have yet to figure out the mechanism)
    • One workaround that I haven't tried: shiv
    • Popular modules, so maybe you don't have to include them…

      python3 -m venv --system-site-packages venv
      • Now you have access to the any Python packages installed on the system