27 Feb 2026

feedDjango community aggregator: Community blog posts

Using tox to Test a Django App Across Multiple Django Versions

Recently, I developed a reusable Django app django-clearplaintext for normalizing plain text in Django templates. And to package and test it properly, I had a fresh look to Tox.

Tox is the standard testing tool that creates isolated virtual environments, installs the exact dependencies you specify, and runs your test suite in each one - all from a single command.

This post walks through a complete, working setup using a minimal example app called django-shorturl.

The Example App: django-shorturl

django-shorturl is a self-contained Django app with one model and one view.

shorturl/models.py

from django.db import models
from django.utils.translation import gettext_lazy as _

class ShortLink(models.Model):
    slug = models.SlugField(_("slug"), unique=True)
    target_url = models.URLField(_("target URL"))
    created_at = models.DateTimeField(_("created at"), auto_now_add=True)

    class Meta:
        verbose_name = _("short link")
        verbose_name_plural = _("short links")

    def __str__(self):
        return self.slug

shorturl/views.py

from django.shortcuts import get_object_or_404, redirect

from .models import ShortLink

def redirect_link(request, slug):
    link = get_object_or_404(ShortLink, slug=slug)
    return redirect(link.target_url)

shorturl/urls.py

from django.urls import path

from . import views

urlpatterns = [
    path("<slug:slug>/", views.redirect_link, name="redirect_link"),
]

shorturl/admin.py

from django.contrib import admin
from .models import ShortLink

admin.site.register(ShortLink)

Project Layout

django-shorturl/
├── src/
│   └── shorturl/
│       ├── __init__.py
│       ├── admin.py
│       ├── models.py
│       ├── views.py
│       └── urls.py
├── tests/
│   ├── __init__.py
│   └── test_views.py
├── pyproject.toml
├── test_settings.py
└── tox.ini

The source lives under src/ and the tests are at the top level, separate from the package. This separation prevents the tests from accidentally being shipped inside the installed package.

Packaging: pyproject.toml

Tox needs a properly packaged app to install into each environment. With isolated_build = true (more on that below), Tox builds a wheel from your pyproject.toml before running any tests.

pyproject.toml

[project]
name = "django-shorturl"
version = "1.0.0"
requires-python = ">=3.8"
dependencies = [
    "Django>=4.2",
]

[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"

[tool.setuptools.packages.find]
where = ["src"]

The dependencies list here declares the runtime minimum - your app needs Django, but you don't pin a specific version because that is Tox's job during testing.

For the [build-system] section, we can also use uv_build to gain some performance improvements:

[build-system]
requires = ["uv_build >= 0.10.0, <0.11.0"]
build-backend = "uv_build"

[tool.uv.build-backend]
module-name = "shorturl"

Here module-name lets uv_build not to get confused between django-shorturl and shorturl.

Test Settings: test_settings.py

Django requires a settings module to run. As we don't have an associated project, we have to create a minimal one by defining project settings in the project's settings, create a minimal one dedicated to testing. It lives at the repo root so it's easy to point to from anywhere.

test_settings.py

SECRET_KEY = "test"

INSTALLED_APPS = [
    "shorturl",
]

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.sqlite3",
        "NAME": ":memory:",
    }
}

ROOT_URLCONF = "shorturl.urls"

DEFAULT_AUTO_FIELD = "django.db.models.AutoField"

A few deliberate choices here:

The Core: tox.ini

This is where Tox is configured.

tox.ini

[tox]
envlist =
    py{38,39,310,311,312}-django42,
    py{310,311,312}-django50,
    py{310,311,312,313}-django51,
    py{310,311,312,313,314}-django52,
    py{312,313,314}-django60

isolated_build = true

[testenv]
deps =
    django42: Django>=4.2,<4.3
    django50: Django>=5.0,<5.1
    django51: Django>=5.1,<5.2
    django52: Django>=5.2,<6.0
    django60: Django>=6.0,<6.1
commands =
    python -m django test
setenv =
    DJANGO_SETTINGS_MODULE = test_settings

envlist - the matrix

py{38,39,310,311,312}-django42 is a shortcut used in Tox.

The numbers inside {} are expanded automatically. Tox combines each Python version with django42, creating 5 environments:

The full envlist simply lists all Python and Django combinations you want to test, so you can check that your project works in each setup.

Each part separated by a dash in an environment name is called a "factor". You can have as many factors as you like, and they can be named anything. py* factors are a convention for Python versions. Others need to be defined in the [testenv] deps section.

isolated_build = true

This tells tox to build a proper wheel from your pyproject.toml before installing into each environment. Without it, tox would try to install your package with pip install -e ., which bypasses the build system and can hide packaging bugs. With it, each environment tests the package exactly as a user would receive it after pip install django-shorturl.

deps - conditional dependencies

The django42: prefix is a Tox factor condition: the dependency on that line is only installed when the environment name contains the django42 factor. This is how a single [testenv] block handles all Django versions without needing a separate section for each one.

Tox also installs your package itself into each environment (because of isolated_build), so you don't need to list it here.

commands

commands =
    python -m django test

python -m django test is Django's built-in test runner. It discovers tests by looking for files matching test*.py under the current directory, which picks up everything in your tests/ folder automatically.

setenv

setenv =
    DJANGO_SETTINGS_MODULE = test_settings

Django refuses to run without a settings module. This environment variable tells it where to find yours. Because test_settings.py is at the repo root and tox runs from the repo root, the module name test_settings resolves correctly without any path manipulation.

Writing the Tests

Create test cases for each (critical) component of your app. For example, if you have models, views, and template tags, create tests/test_models.py, tests/test_views.py, and tests/test_templatetags.py.

tests/test_views.py

from django.test import TestCase
from django.urls import reverse

from shorturl.models import ShortLink


class RedirectLinkViewTest(TestCase):
    def setUp(self):
        ShortLink.objects.create(
            slug="dt",
            target_url="https://www.djangotricks.com",
        )

    def test_redirects_to_target_url(self):
        response = self.client.get(
            reverse(
                "redirect_link", kwargs={"slug": "dt"}
            )
        )
        self.assertRedirects(
            response,
            "https://www.djangotricks.com",
            fetch_redirect_response=False,
        )

    def test_returns_404_for_unknown_slug(self):
        response = self.client.get(
            reverse(
                "redirect_link", kwargs={"slug": "nope"}
            )
        )
        self.assertEqual(response.status_code, 404)

Installing Python Versions with pyenv

Tox needs the actual Python binaries for every version in your envlist. If you try to run tox without them installed, it will fail immediately with an InterpreterNotFound error. pyenv is the standard way to install and manage multiple Python versions side by side.

Install pyenv

Use Homebrew on macOS (or follow the official instructions for Linux):

brew install pyenv

Add the following to your shell config (~/.zshrc, ~/.bashrc, etc.) and restart your shell:

export PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init -)"

Install each Python version

Install every version that appears in your envlist:

pyenv install 3.8
pyenv install 3.9
pyenv install 3.10
pyenv install 3.11
pyenv install 3.12
pyenv install 3.13
pyenv install 3.14

Make them all reachable at once

Tox resolves py312 by looking for a binary named python3.12 on PATH. The trick is pyenv global, which accepts multiple versions and places all of their binaries on your PATH simultaneously:

pyenv global 3.14 3.13 3.12 3.11 3.10 3.9 3.8

List the first (the one python3 and python resolve to) and work downward. After running this, confirm every interpreter is visible:

python3.8 --version   # Python 3.8.x
python3.9 --version   # Python 3.9.x
python3.10 --version   # Python 3.10.x
python3.11 --version   # Python 3.11.x
python3.12 --version   # Python 3.12.x
python3.13 --version   # Python 3.13.x
python3.14 --version   # Python 3.14.x

Now tox can find all of them and the full matrix will run without InterpreterNotFound errors.

Running tox

Run the full matrix:

tox

Or run a single environment:

tox -e py312-django52

tox will print a summary at the end showing which environments passed and which failed.

  py38-django42: OK (3.25=setup[2.32]+cmd[0.93] seconds)
  py39-django42: OK (2.88=setup[2.16]+cmd[0.72] seconds)
  py310-django42: OK (2.61=setup[2.02]+cmd[0.59] seconds)
  py311-django42: OK (2.70=setup[2.09]+cmd[0.61] seconds)
  py312-django42: OK (3.28=setup[2.46]+cmd[0.82] seconds)
  py310-django50: OK (2.67=setup[2.09]+cmd[0.58] seconds)
  py311-django50: OK (2.61=setup[2.02]+cmd[0.59] seconds)
  py312-django50: OK (2.85=setup[2.25]+cmd[0.60] seconds)
  py310-django51: OK (2.81=setup[2.27]+cmd[0.54] seconds)
  py311-django51: OK (2.85=setup[2.30]+cmd[0.55] seconds)
  py312-django51: OK (2.70=setup[2.09]+cmd[0.61] seconds)
  py313-django51: OK (2.97=setup[2.29]+cmd[0.68] seconds)
  py310-django52: OK (3.03=setup[2.31]+cmd[0.72] seconds)
  py311-django52: OK (2.88=setup[2.22]+cmd[0.66] seconds)
  py312-django52: OK (2.80=setup[2.13]+cmd[0.67] seconds)
  py313-django52: OK (4.70=setup[3.66]+cmd[1.04] seconds)
  py314-django52: OK (6.41=setup[5.18]+cmd[1.23] seconds)
  py312-django60: OK (5.13=setup[4.06]+cmd[1.07] seconds)
  py313-django60: OK (5.35=setup[4.15]+cmd[1.21] seconds)
  py314-django60: OK (6.01=setup[4.65]+cmd[1.37] seconds)
  congratulations :) (70.59 seconds)

Final Words

What makes this setup robust?

This setup is not the only way to test a Django app with Tox, but it is a solid starting point that balances comprehensiveness with maintainability. With a little effort upfront, you can ensure your app works across a wide range of Python and Django versions - and catch packaging bugs before they hit real users.

27 Feb 2026 6:00pm GMT

Django News - Google Summer of Code 2026 with Django - Feb 27th 2026

News

Google Summer of Code 2026 with Django

All the information you need to apply for Django's 21st consecutive year in the program.

djangoproject.com

Django Software Foundation

DSF member of the month - Baptiste Mispelon

Baptiste is a long-time Django and Python contributor who co-created the Django Under the Hood conference series and serves on the Ops team maintaining its infrastructure. He has been a DSF member since November 2014. You can learn more about Baptiste by visiting Baptiste's website and his GitHub Profile.

djangoproject.com

Wagtail CMS News

The *1000 most popular* Django packages

Based on GitHub stars and PyPI download numbers.

wagtail.org

Updates to Django

Today, "Updates to Django" is presented by Johanan from Djangonaut Space! 🚀

Last week we had 11 pull requests merged into Django by 10 different contributors - including 4 first-time contributors! Congratulations to Saish Mungase, Marco Aurélio da Rosa Haubrich, 조형준 and Muhammad Usman for having their first commits merged into Django - welcome on board!

This week's Django highlights:

Django Newsletter

Django Fellow Reports

Django Fellow Report - Jacob

A short week with a US holiday and some travel to visit family, but still 4 tickets triaged, 12 reviewed, 3 authored, security report, and more.

djangoproject.com

Django Fellow Report - Natalia

Roughly 70% of my time this week went into security work, which continues being quite demanding. The remaining time was primarily dedicated to Mike's excellent write-up on the dictionary-based EMAIL_PROVIDERS implementation and migration, along with a smaller amount of ticket triage and PR review.

Also 2 tickets triaged, 9 reviewed, and other misc.

djangoproject.com

Sponsored Link 1

PyTV - Free Online Python Conference (March 4th)

1 Day, 15 Speakers, 6 hours of live talks including from Sarah Boyce, Sheena O'Connell, Carlton Gibson, and Will Vincent. Sign up and save the date!

jetbrains.com

Articles

⭐ Django ORM Standalone⁽¹⁾: Querying an existing database

A practical step-by-step guide to using Django ORM in standalone mode to connect to and query an existing database using inspectdb.

paulox.net

Using tox to Test a Django App Across Multiple Django Versions

A practical, production-ready guide to using tox to test your reusable Django app across multiple Python and Django versions, complete with packaging, minimal test settings, and a full version matrix.

djangotricks.com

How I Use django-simple-nav for Dashboards, Command Palettes, and More

Jeff shares how he uses django-simple-nav to define navigation once in Python and reuse it across dashboards and even a lightweight HTMX-powered command palette.

webology.dev

Serving Private Files with Django and S3

Django's FileField and ImageField are good at storing files, but on their own they don't let us control access. When …

lincolnloop.com

CLI subcommands with lazy imports

In case you didn't hear, PEP 810 got accepted which means Python 3.15 is going to support lazy imports! One of the selling points of lazy imports is with code that has a CLI so that you only import code as necessary, making the app a bit more snappy

snarky.ca

Events

DjangoCon US Updated Dates

The conference is now August 24-28, 2026 in Chicago, Illinois. The Call for Proposals (CFP) is open until March 16. And Early Bird Tickets are now available!

djangocon.us

Sponsored Link 2

Sponsor Django News

Reach 4,300+ highly-engaged and experienced Django developers.

django-news.com

Podcasts

Django Chat #196: Freelancing & Community - Andrew Miller

Andrew is a prolific software developer based out of Cambridge, UK. He runs the solo agency Software Crafts, writes regularly, is a former Djangonaut, and co-founder of the AI banking startup Hamilton Rock.

djangochat.com

PyPodcats Episode 11 with Sheena O'Connell

Sheena O'Connell tells us about her journey, the importance of community and good practices for teachers and educators in Python, and organizational psychology. We talk about how to enable a 10x team and how to enable the community through guild of educators.

pypodcats.live

Django Job Board

This week there is a very rare Infrastructure Engineer position for the PSF.

Infrastructure Engineer at Python Software Foundation 🆕

Lead Backend Engineer at TurnTable

Backend Software Developer at Chartwell Resource Group Ltd.

Django Newsletter

Projects

yassi/dj-control-room

The control room for your Django app.

github.com

adamchainz/icu4py

Python bindings to the ICU (International Components for Unicode) library (ICU4C).

github.com

matagus/awesome-django-articles

📚 Articles explaining topics about Django like admin, ORM, views, forms, scaling, performance, testing, deployments, APIs, and more!

github.com

Sponsorship

🚀 Reach 4,300+ Django Developers Every Week

Want to reach developers who actually read what they subscribe to?

Django News is opened by thousands of engaged Django and Python developers every week. A 52% open rate and 15% click rate means your message lands in front of people who pay attention.

Support the newsletter and promote your product, service, event, or job to builders who use Django daily.

👉 Explore sponsorship options: https://django-news.com/sponsorship

django-news.com


This RSS feed is published on https://django-news.com/. You can also subscribe via email.

27 Feb 2026 5:00pm GMT

25 Feb 2026

feedDjango community aggregator: Community blog posts

Freelancing & Community - Andrew Miller

🔗 Links

📦 Projects

📚 Books

🎥 YouTube

Sponsor

This episode is brought to you by Six Feet Up, the Python, Django, and AI experts who solve hard software problems. Whether it's scaling an application, deriving insights from data, or getting results from AI, Six Feet Up helps you move forward faster.

See what's possible at sixfeetup.com.

25 Feb 2026 6:00pm GMT