Home Projects Philosophy Hobbies About Me Updates

Your Python Application as a Single File

When I first started to write Hatta, I made it a single Python script, because I wanted it to be just a single file you can drop into your project and run it. That turned out to be increasingly harder to maintain as the application grew – I even included some data in form of strings in the source file!

Then I discovered that Python can easily run zipped code – there is a nice howto about it. And using pkgutil.get_data you can even load all the data files you need from the same zip file. Oh, and you can include all the pure-python dependencies in the zip.

Moreover, you can add a hashbang and a .py extension, so that it runs with Python by default both on POSIX systems and Windows. Of course, your users still need to have Python installed, and possibly all the non-pure libraries.

Technical details

So how do you exactly do it? Let me show you using my game Jelly as an example. The directory structure looks something like this:

[... more .py files here ...]
[... more .png files here ...]

In the __main__.py is very simple:

#!/usr/bin/env python

from jelly import game


Now, in the game itself I need to load the images and fonts into memory. As mentioned before, I do that using pkgutil.get_data:

def load_image(filename):
    f = StringIO.StringIO(pkgutil.get_data('jelly', filename))
    return pygame.image.load(f, filename).convert()

Unfortunately the trick with StringIO leads to a segmentation fault on some versions of PyGame when we try to load fonts that way. We need a workaround:

def load_font(filename, size=8):
    with tempfile.NamedTemporaryFile() as f:
        data = pkgutil.get_data('jelly', filename)
        font = pygame.font.Font(f.name, size)
    return font

Unfortunately again, that workaround will not work on Windows, due to non-POSIX file operations semantics. Long story short, you need to use this:

def load_font(filename, size=8):
    tmpdir = tempfile.mkdtemp()
    fname = os.path.join(tmpdir, filename)
        with open(fname, 'wb') as f:
            data = pkgutil.get_data('jelly', filename)
        font = pygame.font.Font(fname, size)
    return font

Building the package

Now we can make the zip file:

zip -r jelly.zip jelly/ __main__.py --exclude='*.pyc'

Then we add the hashbang at the beginning. We make a file hashbang.txt with following contents:

#!/usr/bin/env python

(Make sure to have an empty line at the end.) Now just join the two:

cat hashbang.txt jelly.zip > jelly.zip.py
chmod +x jelly.zip.py

And voila! We have our application in a single runnable file!


As mentioned, you can include all your pure-python dependencies in the archive. Unfortunately, you cannot do that with any binary libraries – the users still need to have them installed to run your application. In the Jelly example above, the users would still need to have PyGame installed, for example.