This is an in-progress draft, enjoy.

Managing version compatability

The pace of Django version releases is one of the great benefits of using the framework. On a reliable schedule Django users get not just minor improvements but incredible feature updates.

The downside to new releases is that sometimes APIs change. This means code in your app based on one version needs to change to account for the next version.

In a single Django site deployment, this is fairly simple to account for. When a Django API changes, you make the modifications in your site as you upgrade your Django version, and after testing to ensure everything works as expected, you get to deploy with the updated APIs.

In the case of a reusable app, however, you need to account for different Django versions, often with conflicting or missing APIs. In the vast majority of cases this can be handled easily using a few simple strategies.

Testing

The first step for supporting multiple Django versions is having a testing strategy that supports testing multiple Django versions. The answer here is a tool called tox.

For our purposes tox is a tool that automatically manages Python virtual environments across a matrix of various dependency versions (including Python interpreters) and allows you to run the same tests across each combination of dependencies (and Python interpreters).

Let's say you want to support Django 1.11, Django 2.0, and Django 2.1. Supporting each of these means that your tests run (presumably with coverage to validate that your app does!) and pass using each version. You could manually create virtual environments and install different versions of Django. Or you could tell tox which dependencies to use and then run a single command of tox to run them all.

Here's how you use tox from the command line to run your tests against your specified combination of Django and Python versions:

> tox

As a logistical step, this is pretty simple, and we'll show the "how" below. The single biggest benefit of setting up tox configuration for your app is that now you can start testing for different versions with a change to one or two lines in a configuration file, and each time you run your tests with tox they'll be run against that combination, too.

Choosing which versions to support

There are a number of factors that will influence which Django versions you choose to support. You may choose to support every Django version ever published or just the most bleeding edge version. A good rule of thumb though is to start with the versions of Django supported by the Django project itself.

From here there's nothing wrong with supporting older versions of Django provided that doing so does not pose a burden in the form of flagged features or workarounds.

Python versions

A similar heuristic can be applied for which Python versions to test against. The short answer is to test against all Python versions which your supported Django versions support1.

Django Version Python Versions
1.11 2.7, 3.4, 3.5, 3.6
2.0 3.4, 3.5, 3.6, 3.7
2.1, 2.2 3.5, 3.6, 3.7

Because tox lets you create specified combinations of Python versions and depenencies, you can choose which Django versions get tested with what Python versions. You do not need to test every supported Django version against all Python versions, just the Python versions each supports.

Compatibility APIs

Interfaces for compatability

This is your interface design for your code. And it's good defensive programming in general. Where you have code of your own that may change, or code that refers to Django code that has or will change, wrap it in another function or method so that despite what else may change, app users (including your future self) don't have to worry about their calling code changing.

Compatability modules

You can handle much of the changes within your own code by pushing these checks and imports to a distinct module which encapsulates the necessary changes based on Django or even Python versions.

For instance, let's say your app makes use of Django's mark_safe for marking HTML rendered strings as safe and not requiring escaping. This moved in version 1.6 from XXXXX to ZZZZZ. A natural workaround for this might be:

try:
    from django.utils.text import mark_safe
except ImportError:
    from django.html import mark_safe

Which in and of itself is fine, but after a while seeing your code littered with try/except blocks to import a few functions gets pretty old. A cleaner way is to push all of these conditonal imports into a compatibility module, and import from that module in each calling module.

from myapp.compat import mark_safe

Older Django versions

Supporting older versions of Django concurrently with advanced versions can start to post a problem. For example, if you want to support both Django 1.6 and Django 1.8 (or 1.9), you need to accommodate two different ways of migrating database schemas. Not to mention changes in the Django package API.

In order to support multiple database schema changes, you'll need to be able to toggle settings values based on the Django version. In any Django environment for Django 1.4, you include South as a requirement (the precursor to django's native migrations).

Remember how we called django.setup() in our test file? Now we'll need to make sure we don't do that.

Building schema changes works the same way, and you'll still need to be on the lookout for swappable models for which the field change needs to be edited by hand.

Why would you ever support older, deprecated versions of Django? Well, if you have one or more projects stuck on an older version, then it's probably helpful to keep your own app compatible with said older version. And if barring great complexity or reliance on fast moving features, if the cost of keeping it compatible with older versions is cheap, it enables a greater number of people to use your app. Few people start projects with older versions, at least on purpose, but a lot of projects keep running on older versions due to the cost of upgrading.

1. Version lists are from https://docs.djangoproject.com/en/2.1/faq/install/#faq-python-version-support