Django’s three types of model inheritance
This is part of a series of posts I’m doing as a sort of Python/Django Advent calendar, offering a small tip or piece of information each day from the first Sunday of Advent through Christmas Eve. See the first post for an introduction.
Inheritance and its discontents
People can, and do, debate whether inheritance in object-oriented programming languages is a thing that ought to exist. There are even debates about what “inheritance” ought to mean, because there are multiple things it could mean (see this post by Hillel Wayne for a good brief explanation of the different potential meanings of “inheritance”).
When Django was first released, over 18 years ago, it supported inheritance in its ORM’s model-class definitions, and the project from which Django had been extracted (a news-oriented content-management system) unfortunately made use of this (for why this was unfortunate, you’ll have to keep reading to the end). Then the Django ORM was completely rewritten and, for a brief time, the rewritten version didn’t support inheritance.
But there was consistent pressure to add the feature back, and it was added back. And Django still supports model class inheritance to this day. In fact, it supports three different flavors of inheritance, only one of which you shouldn’t use. So let’s take a quick look the them. For all the details, of course, refer to the Django documentation on model inheritance.
The good one
The best case for inheritance in Django models is abstract models, which can’t be instantiated or saved on their own, but rather define a useful set of base fields or methods (or both) to be inherited into and reused by other models. Here’s an example:
from django.db import models
class Audited(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
abstract = True
You can write other models that subclass Audited
, and they’ll inherit those two fields (created_at
will auto-populate with the current timestamp the first time an object is saved; updated_at
will auto-populate with the current timestamp every time it’s saved).
This is a really really useful feature, and I recommend using it whenever you find yourself repeating a common set of fields or methods or attributes or any other code across a bunch of models: bundle it up into abstract classes and inherit as needed. Quite a few third-party Django applications are able to add new features to models through inheriting from abstract bases.
The rarely-need-it one
Another option is “proxy” inheritance. This is where you subclass another model, and in your child class’ Meta
you declare proxy = True
. This one is a bit weird, but the idea is you’re augmenting the parent class’ behavior. A proxy subclass uses the same database table as the parent class, and cannot add or change model fields, but it can add new methods and it can have its own Meta
declaration and change some behaviors compared to the parent (for example, you can change the default query ordering).
This is somewhat similar to Ruby’s open classes, or extension methods in C#, except that the new behavior only exists on your subclass, and you have to query through your subclass to get instances with that behavior.
It’s also a feature that you need only rarely, but when you do it’s handy; one example of when you’d need it is when you’re using a model class from a third-party package, so you can’t actually reach in and change its behavior, but you can write a proxy subclass of it and decide how you want it to behave.
The please-don’t-use-it one
Finally, there’s “concrete” or “multi-table” inheritance. By default this is what you get when you subclass an existing model and don’t set proxy = True
in the Meta
declaration. Multi-table inheritance works like this:
- A new database table is created for the subclass.
- A
OneToOneField
withparent_link=True, primary_key=True
is created, unless you manually override this, pointing back to the table of the parent model class.
This lets you do things like, say, a base Place
model with subclasses Restaurant
and Shop
and Park
and so on.
Which is what that content-management system did. And what I hope never to deal with again, and hope you never will either.
Multi-table inheritance has a lot of issues:
- It doesn’t make sense conceptually to have something that’s “inheritance” at the Python code level and “composition” (via the foreign key of the
OneToOneField
) at the database level. - It raises questions about what happens when you try to modify or redefine a parent field in the subclass (in Django, you can’t).
- It makes queries of the subclass more complex to reason about, since you need to think about not just the set of fields defined in the child model, but also the parent model, and querying strategies for efficiently fetching them now that your “subclass” is actually spread across multiple database tables.
- It makes queries of the parent model — especially with multiple subclasses! — more complex because you need to test whether each one is “really” supposed to be an instance of a subclass.
And on and on and on. It’s just a headache and so I strongly recommend against it. If you want a common set of fields reused in a bunch of models, do it with an abstract model class, rather than multi-table inheritance.