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
| Style | What it does | Extra 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 freeBecause 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 onMetainheritance: a child does not automatically inherit the parent'sMeta. If you want to extend it, subclass it explicitly withclass Meta(TimeStamped.Meta)— and remember to resetabstract = Falseon the child, or it too becomes abstract.
Example
# 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 DBWhen 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.
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.
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.
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