Navigating the Django-Wagtail-Python upgrade matrix

One axis at a time

Geschrieben von Timo Rieber am 22. Juni 2025

texsite, my open-source Wagtail CMS package, had fallen behind. Django 3.2, Wagtail 4.1, Python 3.8 still in the test matrix. The gap had grown over two years while I focused on other parts of the stack. So one Saturday evening I just sat down and started.

Why incremental

Django, Wagtail, and Python constrain each other. Wagtail 6.0 requires Django 4.2, so you need to get Django there first. Wagtail 6.3 formally supports Python 3.13, but earlier versions don’t. A big-bang jump to the latest of everything ignores the paths through that grid. When everything breaks at once, you can’t tell which upgrade caused which failure.

Each intermediate version has its own deprecation warnings. Wagtail 4.2 tells you that wagtail.contrib.modeladmin is going away. Django 4.0 tells you that ugettext_lazy is deprecated. Fix those, bump to the next version, repeat. If something breaks, you know exactly which step caused it.

The actual sequence

First commit at 20:24, last framework upgrade at 23:10. Fourteen commits, under three hours.

The Wagtail side went first. Upgrading from 4.1 to 4.2 meant removing wagtail.contrib.modeladmin from INSTALLED_APPS and fixing the import path from wagtail.core.models to wagtail.models. Two lines of real change. Going to 5.0 required an explicit SlugInput widget for the slug field in the admin panel:

from wagtail.admin.widgets.slug import SlugInput

FieldPanel('slug', widget=SlugInput)
Python

Wagtail 5.0 through 5.2 LTS was three version bumps with no code changes at all. Tests passed, move on.

With Wagtail at 5.2 LTS, the matrix now supported Python 3.12, so I added it and dropped 3.8. Then continued with Django.

Django 3.2 to 4.0 was the biggest single step. ugettext_lazy became gettext_lazy across four model files, and django.conf.urls.url became django.urls.re_path. Twelve files touched, but all mechanical replacements. Django 4.1 needed a django-bootstrap-ui bump from 2.x to 4.0. Django 4.2 LTS was a version bump only.

Back to Wagtail. The 6.0 upgrade removed use_json_field=True from all StreamField definitions - it became the default. Four model files, one line each:

body = StreamField([
    ('content', ContentBlock()),
    ('documents', DocumentsBlock()),
    ('people', PeopleBlock()),
    ('contact', ContactBlock()),
],)  # use_json_field gone
Python

Then Wagtail 6.1, 6.2, 6.3 LTS - three more bumps, no code changes. Wagtail 6.3 opened up support for Python 3.13, so I added that too.

Final state: Django 4.2 LTS, Wagtail 6.3 LTS, Python 3.9 through 3.13. The tox environment list went from py{38,39,310,311}-dj{32}-wt{41} to py{39,310,311,312,313}-dj{42}-wt{63}.

Out of eleven framework version bumps, only four required actual code changes. The rest were config-only. No business logic touched, no templates rewritten, no database migrations to worry about. The tooling modernisation - migrating to ruff, pyproject.toml - I saved for a separate session. One thing at a time.