Abstract Base Models & Inheritance

Share timestamps, soft-delete flags and common fields across models with abstract base classes.

By the time a project has ten models, you will notice the same three or four fields copied everywhere: created, modified, maybe an is_active flag. Django gives you three ways to share that structure, and picking the right one is a genuinely senior decision.

The three inheritance styles

StyleWhat it doesExtra table?
Abstract base (abstract = True)Copies fields down into each child at migration time.No
Multi-table (concrete parent)Each model gets its own table joined by an implicit OneToOne.Yes — hidden JOINs
Proxy (proxy = True)Same table, different Python behaviour / default ordering / managers.No

For shared fields you almost always want abstract base classes. They keep your schema flat — no surprise JOINs — while letting you write the fields once.

The simplest useful case

from django.db import models

class TimeStamped(models.Model):
    created = models.DateTimeField(auto_now_add=True)
    modified = models.DateTimeField(auto_now=True)

    class Meta:
        abstract = True   # no table is created for TimeStamped

class Article(TimeStamped):
    title = models.CharField(max_length=200)
    # Article now has created + modified for free

Because TimeStamped is abstract, Django never builds a table for it — the fields are physically copied into Article's table. That is exactly what you want for cross-cutting concerns.

A word on Meta inheritance: a child does not automatically inherit the parent's Meta. If you want to extend it, subclass it explicitly with class Meta(TimeStamped.Meta) — and remember to reset abstract = False on the child, or it too becomes abstract.

Example

Example · python
# A reusable soft-delete + timestamp toolkit built from abstract bases.
from django.db import models
from django.utils import timezone


class SoftDeleteQuerySet(models.QuerySet):
    def alive(self):
        return self.filter(deleted_at__isnull=True)

    def dead(self):
        return self.filter(deleted_at__isnull=False)

    def delete(self):
        # Bulk soft-delete instead of hitting the DB DELETE.
        return self.update(deleted_at=timezone.now())


class SoftDeleteManager(models.Manager):
    def get_queryset(self):
        # Hide soft-deleted rows by default.
        return SoftDeleteQuerySet(self.model, using=self._db).alive()


class TimeStamped(models.Model):
    created = models.DateTimeField(auto_now_add=True)
    modified = models.DateTimeField(auto_now=True)

    class Meta:
        abstract = True


class SoftDelete(models.Model):
    deleted_at = models.DateTimeField(null=True, blank=True, editable=False)

    objects = SoftDeleteManager()          # filtered: only alive rows
    all_objects = models.Manager()          # escape hatch: everything

    def delete(self, using=None, keep_parents=False):
        self.deleted_at = timezone.now()
        self.save(update_fields=['deleted_at'])

    class Meta:
        abstract = True


class Invoice(TimeStamped, SoftDelete):
    number = models.CharField(max_length=20, unique=True)
    amount = models.DecimalField(max_digits=10, decimal_places=2)

    class Meta:
        # Extend a parent's Meta explicitly; reset abstract on the concrete child.
        ordering = ['-created']

# Invoice.objects.all()      -> only alive invoices
# Invoice.all_objects.all()  -> alive + soft-deleted
# invoice.delete()           -> sets deleted_at, row stays in the DB

When to use it

  • A developer defines a TimeStampedModel abstract base with created_at and updated_at so every concrete model inherits timestamps.
  • A team creates an OwnedModel abstract class with an owner ForeignKey so all resource models share ownership logic.
  • A developer uses an abstract SoftDeleteModel to add a deleted_at field and override delete() across multiple models.

More examples

Abstract timestamp mixin

Defines an abstract base model with timestamps; Post inherits both fields without duplicating code.

Example · python
from django.db import models

class TimeStampedModel(models.Model):
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        abstract = True

class Post(TimeStampedModel):
    title = models.CharField(max_length=250)

Abstract model with default manager

Adds publish() logic to an abstract model so any subclass inherits the publishing workflow.

Example · python
class PublishableModel(models.Model):
    is_published = models.BooleanField(default=False)
    published_at = models.DateTimeField(null=True, blank=True)

    class Meta:
        abstract = True

    def publish(self):
        from django.utils import timezone
        self.is_published = True
        self.published_at = timezone.now()
        self.save(update_fields=["is_published", "published_at"])

Soft-delete abstract model

Overrides delete() to set a timestamp instead of removing the row, enabling soft-delete behavior.

Example · python
class SoftDeleteModel(models.Model):
    deleted_at = models.DateTimeField(null=True, blank=True)

    class Meta:
        abstract = True

    def delete(self, *args, **kwargs):
        from django.utils import timezone
        self.deleted_at = timezone.now()
        self.save(update_fields=["deleted_at"])

    @property
    def is_deleted(self):
        return self.deleted_at is not None

Discussion

  • Be the first to comment on this lesson.