I’ve been experimenting a bit with the new zipapp module in python 3.5.

(Yes, it’s 3.5, I use 3.5 now, 3.5 is cool)

From the documentation:

This module provides tools to manage the creation of zip files containing Python code, which can be executed directly by the Python interpreter. The module provides both a Command-Line Interface and a Python API.

So say we have written a little CLI tool. We can now package this up as a single file and execute it. Neat!

Here is how it works.

First thing to know: If you place a __main__.py, you can execute the directory (package) with python.

Our directory looks like this

app
├── __main__.py
├── otherfile.py
└── requirements.txt

and our __main__.py like this

print("hello world")

We can now run python with the app directory as argument:

$> python app
hello world

Another neat feature is that it works even if you zip the app directory.

$> pushd app
~/src/demo/app ~/src/demo
$> zip -r ../app.pyz *
  adding: __main__.py (stored 0%)
  adding: requirements.txt (stored 0%)
$> popd
~/src/demo
# Now let's run the archive
$> python app.pyz
hello world

We can even add a shebang to the start of the file:

$> echo '#!/usr/bin/env python' > app.cmd
$> cat app.pyz >> app.cmd 
$> chmod +x app.cmd
$> ./app.cmd
hello world

And this is how we produce a single-file python executable.

You’ve been able to execute a zipfile with python since before time (it is present in 2.6, so it is oooold). So you should be able to use this just about anywhere you have a somewhat updated python version.

In Python 3.5

New in Python 3.5 is the zipapp module, which wraps all this functionality in a nice little module. So instead of all the above, we can now just do:

$> python3.5 -m zipapp --python '/usr/bin/env python' --output app.pyz app
$> ./app.pyz
hello world

Which does pretty much what we did above.

Dependencies

Of course this only bundles our own code, what if we have dependencies? Sorry, you’ll have to bundle them yourself. Luckily, this is super easy!

Simply install your dependencies inside your application directory and add it to your sys.path.

You could do it like:

$> python -m pip install --prefix ./app click
Collecting click
  Using cached click-6.2-py2.py3-none-any.whl
Installing collected packages: click
Successfully installed click

Resulting in a directory structure like this:

app
├── lib
│   └── python2.7
│       └── site-packages
│           ├── click
│           └── click-6.2.dist-info
├── __main__.py
└── requirements.txt

(note that i used python2.7)

and in your __main__.py, add it to your sys.path

import sys
import os
sys.path.append(
    os.path.join(
        os.path.dirname(__file__),
        'lib',
        'python%d.%d' % (sys.version_info.major, sys.version_info.minor),
        'site-packages'
    )
)

And you should now be able to load click.

$> python app
<module 'click' from '/home/tbug/src/pybundle/app/lib/python2.7/site-packages/click/__init__.pyc'>

Like always, path hacking feels a little dirty, but we’ll manage.

PyBundle.sh

I wrapped the above examples in a neat little shell-script that does all of this for you.

It also contains a hello-world click example.

Conclusion

Now, is this useful? Where should we use it?

I’m not sure. But it is really cool! :)