Model or dataset
wagtail/django-modelcluster avatar
wagtail/django-modelcluster

django-modelcluster lets a Django model hold its children before the database does

Django extension to allow working with 'clusters' of models as a single unit, independently of the database

519 stars73 forksPythonBSD-3-Clause

At a glance

What is it?
django-modelcluster adds a ParentalKey relation to Django so a parent model and its related objects can be built, queried and modified in memory, and written to the database only when the parent is saved. It suits a Wagtail-adjacent Django project with draft states, previews or revision workflows, and it deliberately supports only a subset of the QuerySet API.
Who is it for?
django-modelcluster fits a Django project whose models need to exist in an incomplete or unsaved state for a while, such as a preview, a draft or a revision, because the ParentalKey relation keeps the children local until the parent is saved. It does not fit a project that needs the full QuerySet API on unsaved children, because the README is explicit that only a subset is supported.
Can I use it commercially?
Yes. BSD-3-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 175 days ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The problem it solves, stated as a database error

The README opens with a data model you have written a thousand times.

python
 class Band(models.Model):
     name = models.CharField(max_length=255)

 class BandMember(models.Model):
     band = models.ForeignKey('Band', related_name='members', on_delete=models.CASCADE)
     name = models.CharField(max_length=255)

And then the thing you would like to do.

python
 beatles = Band(name='The Beatles')
 beatles.members = [
     BandMember(name='John Lennon'),
     BandMember(name='Paul McCartney'),
 ]

You cannot. Objects need to exist in the database for foreign key relations to work, and the failure is a constraint violation.

python
 IntegrityError: null value in column "band_id" violates not-null constraint

That error is the whole reason this library exists. It is not a limitation of Django so much as a consequence of how a relational database works: a child row has to point at a row that exists, so you cannot hold a tree of objects in memory and save it later without either deferring the writes or dropping the relation.

The scenarios the README lists for wanting the other behaviour are the useful part. You might want to render a preview of the data the user has just submitted, prior to saving. You might need to construct a tree of things, serialize them and hand them off to some external system. You might have a workflow where your models exist in an incomplete draft state for an extended time, or you need to handle multiple revisions, and you do not want to redesign your database around that requirement.

Those are all cases where the database should not be the thing holding your working state, and the library is a way to get that without inventing a second persistence layer.

ParentalKey, and what the subset of the QuerySet API covers

django-modelcluster extends Django's foreign key relations to make this possible, and it does so by introducing a new type of relation, ParentalKey, where the related models are stored locally to the parent model until the parent is explicitly saved.

The usage is a small change to the model definition.

python
 from modelcluster.models import ClusterableModel
 from modelcluster.fields import ParentalKey


 class Band(ClusterableModel):
     name = models.CharField(max_length=255)

 class BandMember(models.Model):
     band = ParentalKey('Band', related_name='members', on_delete=models.CASCADE)
     name = models.CharField(max_length=255)

Two things change. The parent model inherits from ClusterableModel instead of models.Model, and the child relation becomes a ParentalKey with the same arguments a ForeignKey takes, so the related_name and on_delete work as you would expect.

Up to the point the parent is saved, the related models can still be accessed through a subset of the QuerySet API, and the shell transcript shows what that means in practice.

python
 >>> beatles = Band(name='The Beatles')
 >>> beatles.members = [
 ...     BandMember(name='John Lennon'),
 ...     BandMember(name='Paul McCartney'),
 ... ]
 >>> [member.name for member in beatles.members.all()]
 ['John Lennon', 'Paul McCartney']
 >>> beatles.members.add(BandMember(name='George Harrison'))
 >>> beatles.members.count()
 3
 >>> beatles.save()  # only now are the records written to the database

So assignment, add, count and all all work on an unsaved parent. The word subset is doing real work in that sentence though, and the README does not enumerate which operations are supported, so you should check the unit tests for the cases you need rather than assuming the QuerySet API is complete. The README points at those tests for more examples, and that is the reference to use.

The many-to-many variant, and the asymmetry in what it stores

For many-to-many relations, a corresponding ParentalManyToManyField is available.

python
 from modelcluster.models import ClusterableModel
 from modelcluster.fields import ParentalManyToManyField

 class Movie(ClusterableModel):
     title = models.CharField(max_length=255)
     actors = ParentalManyToManyField('Actor', related_name='movies')

 class Actor(models.Model):
     name = models.CharField(max_length=255)

And the behaviour, again from the shell.

python
 >>> harrison_ford = Actor.objects.create(name='Harrison Ford')
 >>> carrie_fisher = Actor.objects.create(name='Carrie Fisher')
 >>> star_wars = Movie(title='Star Wars')
 >>> star_wars.actors = [harrison_ford, carrie_fisher]
 >>> blade_runner = Movie(title='Blade Runner')
 >>> blade_runner.actors.add(harrison_ford)
 >>> star_wars.actors.count()
 2
 >>> [movie.title for movie in harrison_ford.movies.all()]  # the Movie records are not in the database yet
 []
 >>> star_wars.save()  # Star Wars now exists in the database (along with the 'actor' relations)
 >>> [movie.title for movie in harrison_ford.movies.all()]
 ['Star Wars']

That last pair of lines is the important one. The relation from the Actor back to the Movie is empty until the Movie is saved, and appears afterwards, which is the reverse of what a plain ManyToManyField does.

There are two structural details the README spells out. ParentalManyToManyField is defined on the parent model rather than the related model, just as a standard ManyToManyField would be. And the related objects, the Actor instances in the example, must exist in the database before being associated with the parent record.

That last restriction is the asymmetry worth planning around. The field lets the relations between Movies and Actors be stored in memory without writing to the database, but not the Actor records themselves. So the parent side of a many-to-many is deferred, and the other side is not.

Introspection, for when you need to copy a model with its children

If you need to find out which child relations exist on a parent model, to create a deep copy of the model and all its children, say, there is a function for it.

python
 >>> from modelcluster.models import get_all_child_relations
 >>> get_all_child_relations(Band)
 [<RelatedObject: tests:bandmember related to band>, <RelatedObject: tests:album related to band>]

This includes relations that are defined on any superclasses of the parent model, which matters for a Wagtail-style hierarchy where a base page model carries children that a subclass inherits.

There is a matching one for the many-to-many fields.

python
 >>> from modelcluster.models import get_all_child_m2m_relations
 >>> get_all_child_m2m_relations(Movie)
 [<modelcluster.fields.ParentalManyToManyField: actors>]

Two functions that tell you what is attached to a model, without you having to know the relations by hand, are the difference between a deep copy that works and one that silently drops children. That is exactly the use case the README names, and it is a common one in a CMS where pages hold blocks that hold children.

The names are slightly asymmetric, get_all_child_relations for the ParentalKey relations and get_all_child_m2m_relations for the many-to-many fields, so it is worth remembering both rather than assuming one covers the other.

Packaging, and the taggit integration

The package metadata is short and tells you the supported range.

toml
[project]
name = "django-modelcluster"
version = "6.5"
description = "Django extension to allow working with 'clusters' of models as a single unit, independently of the database"
readme = "README.rst"
keywords = [ "cluster", "django", "model" ]
license = "BSD-3-Clause"
requires-python = ">=3.10"

So it needs Python 3.10 or newer and the classifiers list Django support from 3.10 through 3.14, with the Development Status recorded as 5 - Production/Stable. The single runtime dependency is django>=4.2.

There is one optional dependency group, `taggit = [ "django-taggit>=3.1" ]`, which is the integration with django-taggit, the tagging library that Wagtail is built on. That is a sensible thing to make optional rather than required, since it ties the library to a specific tagging approach.

The project is maintained by the Wagtail team, with Matthew Westcott and the Wagtail Team address listed as authors, and it is BSD-3-Clause licensed with the LICENSE file at the repository root.

The release history is steady. The listed releases are v6.5 from 2026-05-01, v6.4.1 from 2025-12-04 and v6.4 from 2024-12-18, and the last push to the repository was on 2026-05-01.

The repository layout is a plain Python package with modelcluster/ at the root, tests/ and a runtests.py runner, tox.ini for the test matrix, a ruff.toml for linting and a .pre-commit-config.yaml for the hooks. The documentation itself is README.rst rather than a separate docs tree, which tells you the library is small enough that the readme is the manual.

Where django-modelcluster is the wrong choice

Four cases argue against it.

The first is a project that needs the full QuerySet API on unsaved children. The README says a subset of the QuerySet API is supported and points at the unit tests for the details, which means the coverage is real but partial. If your code is going to issue arbitrary queries against a child relation, you need to check that every query you use is in the supported set, and the tests are the only source of truth.

The second is a project that has already solved this with a different approach. Django has had long-lived patterns for this, the transaction on save, an explicit form or serializer layer that holds the unsaved state, or a draft model with a status column. If your project already has a working answer, adding a second model relation type is a migration cost for no gain.

The third is a project whose children are created and saved individually by design. ParentalKey's value is that children live in memory with their parent. If your child records are meaningful on their own, with their own lifecycle and their own rows in the database, then deferring their creation to the parent's save is the wrong model.

The alternative in the same direction is to keep the ForeignKey and do the two-phase write yourself, saving the parent first and then the children with the parent id set. That is a handful of lines and it keeps the whole of Django's ORM available. The difference in approach is that modelcluster moves the deferral into the relation type, which means the behaviour is automatic and consistent, where the manual version is code you have to remember to write correctly in every place a parent is created.

Editorial conclusion

django-modelcluster fits a Django project whose models need to exist in an incomplete or unsaved state for a while, such as a preview, a draft or a revision, because the ParentalKey relation keeps the children local until the parent is saved. It does not fit a project that needs the full QuerySet API on unsaved children, because the README is explicit that only a subset is supported. Depend on `django-modelcluster` with Django 4.2 or newer and Python 3.10 or newer, use `get_all_child_relations` when you need to copy a model with its children, and remember that the many-to-many variant still requires the related records to exist in the database first.

Frequently asked questions

What does django-modelcluster do?

It extends Django's foreign key relations with a ParentalKey type, so the related models are stored locally to the parent model until the parent is explicitly saved. Up to that point the related models can still be accessed through a subset of the QuerySet API, so you can build a tree of objects in memory without hitting the database.

How do I use ParentalKey in a Django model?

Make the parent model inherit from ClusterableModel and use ParentalKey instead of ForeignKey for the child relation, with the same arguments. Then assign or add children to the parent's related manager, and call save on the parent to write everything to the database.

What is ParentalManyToManyField and what does it not defer?

It is the many-to-many counterpart, defined on the parent model just as a standard ManyToManyField would be. It lets the relations between the parent and the related records be stored in memory without writing to the database, but the related records themselves must already exist in the database before being associated.

How do I find all the child relations on a model?

Use `modelcluster.models.get_all_child_relations`, which returns the child relations on a parent model including those defined on any superclasses. For the many-to-many fields there is `modelcluster.models.get_all_child_m2m_relations`. The README names deep-copying a model with all its children as the motivating case.

What versions of Python and Django does django-modelcluster support?

Python 3.10 or newer and Django 4.2 or newer. The classifiers list Django from 3.10 through 3.14, the runtime dependency is django>=4.2, and there is an optional extra for django-taggit>=3.1. The current release is 6.5 from 2026-05-01.

Official sources

  1. Issues
  2. License: BSD-3-Clause
  3. README
  4. Releases
  5. wagtail/django-modelcluster on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/wagtail-django-modelcluster.svg)](https://hysenlabs.com/projects/wagtail-django-modelcluster)

Was this useful?