Models inheritance works the same way as normal Python class inheritance works, the only difference is, whether we want the parent models to have their own table in the database or not. When the parent model tables are not created as tables it just acts as a container for common fields and methods. We will see examples to understand it in detail.  There are three styles of inheritance possible in Django.

  1. Abstract base classes: Use this when the parent class contains common fields and the parent class table is not desirable.
  2. Multi-table inheritance: Use this when the parent class has common fields, but the parent class table also exists in the database all by itself.
  3. Proxy models: Use this when you want to modify the behavior of the parent class, like by changing orderby or a new model manager.

In this blog, we will discuss the Abstract base classes and Multi-table inheritance. Let's go through examples to understand in detail.

Abstract base classes:

from django.db import models


class BaseModel(models.Model):
    created_at = models.DateTimeField(auto_add_now=True)
    updated_at = models.DateTimeField(auto_now=True)
    
    class Meta:
        abstract = True
 

class Address(BaseModel):
    state = models.CharField(max_length=10)
    country = models.CharField(max_length=10)
    pin_code = models.IntegerField()

In the above example, when we run makemigrations for the above model changes, only Address changes will be there. Please note that in the Meta class of BaseClass we have put abstract=True, this tells Django, not to create a database table for the corresponding table.

class Migration(migrations.Migration):

    initial = True

    dependencies = [
    ]

    operations = [
        migrations.CreateModel(
            name='Address',
            fields=[
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('created_at', models.DateTimeField(auto_now_add=True)),
                ('updated_at', models.DateTimeField(auto_now=True)),
                ('state', models.CharField(max_length=10)),
                ('country', models.CharField(max_length=10)),
                ('pin_code', models.IntegerField()),
            ],
            options={
                'abstract': False,
            },
        ),
    ]

Going forward if we inherit from the BaseModel, that would by default add created_at and updated_at, with this, we achieve DRY (Don't Repeat Yourself) and a cleaner way to code. Let's see another example with a method in BaseModel.

In the above example, childClass now has a nice method to soft-delete objects. Let's see, how it works.

from django.contrib.auth.models import User
from django.db import models
from django.utils import timezone


class BaseSoftDeletableModel(models.Model):
    is_deleted = models.Boolean(default=False)
    deleted_at = models.DateTimeField(null=True)
    deleted_by = models.ForeignKey(User, null=True)
    
    class Meta:
        abstract = True
        
    def soft_delete(self, user_id=None):
        self.is_deleted = True
        self.deleted_by = user_id
        self.deleted_at = timezone.now()
        self.save()


class Address(BaseModel, BaseSoftDeletableModel):
    state = models.CharField(max=10)
    country = models.CharField(max=10)
    pin_code = models.IntegerField()
In [4]: address = Address.objects.create(state='KR', country='IN', pin_code=111111)


In [5]: address.__dict__
Out[5]: 
{
    '_state': <django.db.models.base.ModelState at 0x113d15fa0>,
     'id': 1,
     'created_at': datetime.datetime(2021, 7, 2, 6, 53, 57, 140334, tzinfo=<UTC>),
     'updated_at': datetime.datetime(2021, 7, 2, 6, 53, 57, 140368, tzinfo=<UTC>),
     'is_deleted': False,
     'deleted_by_id': None,
     'deleted_at': None,
     'state': 'KR',
     'country': 'IN',
     'pin_code': 111111
 }


In [6]: address.soft_delete()

In [7]: address.__dict__
Out[7]: 
{
    '_state': <django.db.models.base.ModelState at 0x113d15fa0>,
     'id': 1,
     'created_at': datetime.datetime(2021, 7, 2, 6, 53, 57, 140334, tzinfo=<UTC>),
     'updated_at': datetime.datetime(2021, 7, 2, 6, 54, 39, 496424, tzinfo=<UTC>),
     'is_deleted': True,
     'deleted_by_id': None,
     'deleted_at': datetime.datetime(2021, 7, 2, 6, 54, 39, 495744, tzinfo=<UTC>),
     'state': 'KR',
     'country': 'IN',
     'pin_code': 111111
 }

Multi-table Inheritance:

In the above example of Address class, although it is inheriting from multiple parent tables, it is still an example of the abstract base class inheritance style, since both the parent classes are abstract in nature. For multi-table inheritance, parentClass's table also gets created in the database. The name multi-table comes from the fact that multiple tables actually gets created in the database and not because the childClass is inheriting multiple parentClass. Let's see an example to understand it further.

from django.contrib.auth.models import User
from django.db import models
from django.utils import timezone


class BaseSoftDeletableModel(models.Model):
    is_deleted = models.Boolean(default=False)
    deleted_at = models.DateTimeField(null=True)
    deleted_by = models.ForeignKey(User, null=True)
    
    class Meta:
        abstract = True
        
    def soft_delete(self, user_id=None):
        self.is_deleted = True
        self.deleted_by = user_id
        self.deleted_at = timezone.now()
        self.save()


class Address(BaseModel, BaseSoftDeletableModel):
    state = models.CharField(max=10)
    country = models.CharField(max=10)
    pin_code = models.IntegerField()
    

class Place(Address):
    name = models.CharField(max=10)

Now, when we create a Place instance, it will also create an entry for address in the database. Let's see an example.

In [6]: place = Place.objects.create(name='home', state='KR', country='IN', pin_code=13123)

In [7]: place.__dict__
Out[7]: 
{
    '_state': <django.db.models.base.ModelState at 0x10f8ce1f0>,
     'id': 2,
     'created_at': datetime.datetime(2021, 7, 3, 11, 43, 40, 813214, tzinfo=<UTC>),
     'updated_at': datetime.datetime(2021, 7, 3, 11, 43, 40, 813256, tzinfo=<UTC>),
     'is_deleted': False,
     'deleted_by_id': None,
     'deleted_at': None,
     'state': 'KR',
     'country': 'IN',
     'pin_code': 13123,
     'address_ptr_id': 2,
     'name': 'home'
 }

In [8]: Address.objects.count()
Out[8]: 2

And there you have it, 1 entry for address also got created.

To read more on this topic please read the following reference.

Models | Django documentation | Django
Modeling Polymorphism in Django With Python – Real Python
Modeling polymorphism in relational databases can be a challenging task, but in this article, you’ll learn several modeling techniques to represent polymorphic objects in a relational database using the Django object-relational mapping (ORM).