How to Create and Serve Zipfiles from Django

Bob, Wed 08 May 2019, Django

admin, archive, DB, Django, migrations, request, response, snippets, sqlite3, virtualenv, zipfile

We added support to our platfom for bulk downloading of all your code submissions. This feature required creating and serving up zipfiles through Django. In this article I show you how to do it creating a simple Django app collecting code snippets through the admin interface, and serving them up in a zipfile via a download endpoint. Let's dive straight in ...


First we make a virtual env, set a secret key in our venv and install Django:

[[email protected] code]$ mkdir django-archive
[[email protected] code]$ cd $_
[[email protected] django-archive]$ python3.7 -m venv venv
[[email protected] django-archive]$ echo "export SECRET_KEY='abc123.;#'" >> venv/bin/activate
[[email protected] django-archive]$ source venv/bin/activate
(venv) [[email protected] django-archive]$ pip install django
Collecting django
Successfully installed django-2.2 pytz-2019.1 sqlparse-0.3.0

Now let's create a project and app in Django. Don't forget the extra dot in the startproject command to not create an extra subdirectory.

(venv) [[email protected] django-archive]$ django-admin startproject snippets .
(venv) [[email protected] django-archive]$ django-admin startapp archive
(venv) [[email protected] django-archive]$ tree -L 2
├── archive
   ├── migrations
├── snippets
└── venv

Make sure we add the new app to Django's config:


    # own apps

While here, let's also load the secret key from our venv (venv/bin/activate) as defined earlier:

SECRET_KEY = os.environ['SECRET_KEY']

Lastly let's sync the pending migrations to our default sqlite DB and create a superuser to access Django's admin back-end:

(venv) [[email protected] django-archive]$ python migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, sessions
Running migrations:
Applying contenttypes.0001_initial... OK
Applying auth.0001_initial... OK
Applying admin.0001_initial... OK
Applying admin.0002_logentry_remove_auto_add... OK
Applying admin.0003_logentry_add_action_flag_choices... OK
Applying contenttypes.0002_remove_content_type_name... OK
Applying auth.0002_alter_permission_name_max_length... OK
Applying auth.0003_alter_user_email_max_length... OK
Applying auth.0004_alter_user_username_opts... OK
Applying auth.0005_alter_user_last_login_null... OK
Applying auth.0006_require_contenttypes_0002... OK
Applying auth.0007_alter_validators_add_error_messages... OK
Applying auth.0008_alter_user_username_max_length... OK
Applying auth.0009_alter_user_last_name_max_length... OK
Applying auth.0010_alter_group_name_max_length... OK
Applying auth.0011_update_proxy_permissions... OK
Applying sessions.0001_initial... OK

(venv) [[email protected] django-archive]$ python createsuperuser
Username (leave blank to use 'bobbelderbos'): bob
Email address:
Password (again):
This password is too short. It must contain at least 8 characters.
Bypass password validation and create user anyway? [y/N]: y
Superuser created successfully.

Create routes

In the main app snippets, that was created with the startproject command, we add the following routes:


from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('', include('archive.urls', namespace='archive')),

Apart from the default admin routes (, we namespace the archive app's routes, defining them in the app:


from django.urls import path

from . import views

app_name = 'archive'
urlpatterns = [
    path('download/',, name='download')

This will be the download endpoint that will serve the zipfile, we will write that code in a bit. First let's define the model (DB table) that will hold our code snippets.

Create a Script model

In our archive app we make this simple model and sync it to the DB:


from django.db import models

class Script(models.Model):
    name = models.CharField(max_length=100)
    code = models.TextField()
    added = models.DateTimeField(auto_now_add=True)

    def __str__(self):

    class Meta:
        ordering = ['-added']

We inherit all goodness from Django's Model class. The added datetime gets automatically populated upon insert. Defining a __str__ on the class makes it easier to inspect the objects when debugging (or in Django's interactive shell). And we can use the inner Meta class to set further behaviors, in this case let's show most recently added snippets first.

Now we have to commit ("migrate") this model to the DB which is easy using Django's However first we need to stub out the download function we defined in archive/, otherwise we get: AttributeError: module 'archive.views' has no attribute 'download' upon migration. Add this code to archive/


def download(request):

Now it should work:

(venv) [bobbelderbos@imac django-archive]$ python makemigrations
Migrations for 'archive':
    - Create model Script
(venv) [bobbelderbos@imac django-archive]$ python migrate
Operations to perform:
Apply all migrations: admin, archive, auth, contenttypes, sessions
Running migrations:
Applying archive.0001_initial... OK

I am just using the default sqlite DB, we can use schema to see what migrate created:

(venv) [bobbelderbos@imac django-archive]$ sqlite3 db.sqlite3
SQLite version 3.24.0 2018-06-04 19:24:41
Enter ".help" for usage hints.
sqlite> .table
... other tables ...
archive_script              <== our new table
sqlite> .schema  archive_script
CREATE TABLE IF NOT EXISTS "archive_script" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "name" varchar(100) NOT NULL, "code" text NOT NULL, "added" datetime NOT NULL);

Django's admin interface

To be able to work with the new model from the admin interface we need to register it. Add this code to the archive/ module:


from django.contrib import admin

from .models import Script

class ScriptAdmin(admin.ModelAdmin):
    pass, ScriptAdmin)

Now let's spin up the dev server. As I leave it running in the foreground I use a second terminal:

$ cd /Users/bbelderbos/code/django-archive
$ source venv/bin/activate
(venv) [bbelderbos@imac django-archive]$ python runserver
Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).
May 08, 2019 - 02:17:32
Django version 2.2, using settings 'snippets.settings'
Starting development server at
Quit the server with CONTROL-C.

Now I can access http://localhost:8000/admin and login with the superuser I created earlier. At this point we should see the new model:

new model in admin

Let's add some small code snippets from our tips page:

3 snippets added

adding snippet 1

adding snippet 2

adding snippet 3

Serving up a zipfile

Now let's create a zipfile with all the code snippets stored in the DB. We do this in the download view we stubbed out earlier:


import zipfile

from django.http import HttpResponse

from .models import Script

## PyBites Code Snippet Archive

Here is a zipfile with some useful code snippets.

Produced for blog post

Keep calm and code in Python!

def download(request):
    """Download archive zip file of code snippets"""
    response = HttpResponse(content_type='application/zip')
    zf = zipfile.ZipFile(response, 'w')

    # create the zipfile in memory using writestr
    # add a readme

    # retrieve snippets from ORM and them to zipfile
    scripts = Script.objects.all()
    for snippet in scripts:
        zf.writestr(, snippet.code)

    # return as zipfile
    response['Content-Disposition'] = f'attachment; filename={ZIPFILE_NAME}'
    return response

We use Django's HttpResponse object which we have to give a Content-Disposition attribute. To directly serve up the resulting zipfile, not writing it to disk, I use zipfile's writestr. Getting the snippets from Django's ORM is as easy as: Script.objects.all(). I also added a README file.

Now visit the download endpoint: http://localhost:8000/download -> a zipfile should automatically download to your desktop:

download the zipfile

Let's see if it worked by unzipping the obtained zipfile into a tmp directory:

[bbelderbos@imac Downloads]$ mkdir tmp
[bbelderbos@imac Downloads]$ mv tmp
[bbelderbos@imac Downloads]$ cd tmp
[bbelderbos@imac tmp]$ unzip

[bbelderbos@imac tmp]$ cat

## PyBites Code Snippet Archive

Here is a zipfile with some useful code snippets.

Produced for blog post

Keep calm and code in Python!

[bbelderbos@imac tmp]$ for i in *py; do echo "== $i =="; cat $i; echo ; done
== ==
names = 'bob julian tim sara'.split()
for i, name in enumerate(names, 1):
    print(i, name)
== ==
list_of_lists = [[1, 2], [3], [4, 5], [6, 7, 8]]
flattened = sum(list_of_lists, [])
== ==
names = 'bob julian tim sara'.split()
ages = '11 22 33 44'.split()
print(dict(zip(names, ages)))

Cool! So there you have it: a small Django app with a single model and view to serve zipfiles :)

One enhancement would be to lock this down for users that are not logged in. Django makes this easy, just add this the following code at the top of the download function, returning a 401 (and toast message) if the user is not authenticated:

from django.contrib import messages
def download(request):
    """Download archive zip file of code snippets"""

    if not request.user.is_authenticated:
        messages.error(request, 'Need to be logged in to access this endpoint')
        return HttpResponse(status=401)


The full code for this blog post is in our blog code repo.

If you saved some code for Bite exercises on our platform you can check out this feature scrolling to the bottom of the settings page:

the new platform code zip export button

I hope this was useful and let us know if there are other Django related topics you'd like to see covered here ...

Keep Calm and Code in Python!

-- Bob

PyBites Python Tips

Do you want to get 250+ concise and applicable Python tips in an ebook that will cost you less than 10 bucks (future updates included), check it out here.

Get our Python Tips Book

"The discussions are succinct yet thorough enough to give you a solid grasp of the particular problem. I just wish I would have had this book when I started learning Python." - Daniel H

"Bob and Julian are the masters at aggregating these small snippets of code that can really make certain aspects of coding easier." - Jesse B

"This is now my favourite first Python go-to reference." - Anthony L

"Do you ever go on one of those cooking websites for a recipe and have to scroll for what feels like an eternity to get to the ingredients and the 4 steps the recipe actually takes? This is the opposite of that." - Sergio S

Get the book