Model Inheritance Patterns

+15 Mana ✨

Django supports three types of model inheritance, each with different use cases and database implications.

1. Abstract Base Classes

Share common fields without creating a database table:

python
class BaseContent(models.Model):
    """Abstract base - no table created."""
    title = models.CharField(max_length=200)
    slug = models.SlugField(unique=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    is_published = models.BooleanField(default=False)

    class Meta:
        abstract = True
        ordering = ['-created_at']

    def publish(self):
        self.is_published = True
        self.save()


class Article(BaseContent):
    """Creates table: appname_article with all fields."""
    body = models.TextField()
    author = models.ForeignKey('Author', on_delete=models.CASCADE)


class Video(BaseContent):
    """Creates table: appname_video with all fields."""
    url = models.URLField()
    duration = models.IntegerField(help_text='Duration in seconds')

Database Result:

  • No basecontent table
  • article table with: title, slug, created_at, updated_at, is_published, body, author_id
  • video table with: title, slug, created_at, updated_at, is_published, url, duration

2. Multi-table Inheritance

Each model gets its own table, linked by foreign key:

python
class Place(models.Model):
    """Creates table: appname_place."""
    name = models.CharField(max_length=100)
    address = models.CharField(max_length=200)


class Restaurant(Place):
    """Creates table: appname_restaurant with FK to place."""
    serves_pizza = models.BooleanField(default=False)
    serves_pasta = models.BooleanField(default=False)


class Cafe(Place):
    """Creates table: appname_cafe with FK to place."""
    serves_coffee = models.BooleanField(default=True)
    has_wifi = models.BooleanField(default=True)

Database Result:

  • place table: id, name, address
  • restaurant table: place_ptr_id (FK), serves_pizza, serves_pasta
  • cafe table: place_ptr_id (FK), serves_coffee, has_wifi

Usage:

python
# Create a restaurant
r = Restaurant.objects.create(
    name='Pizza Palace',
    address='123 Main St',
    serves_pizza=True
)

# Access parent fields directly
print(r.name)  # 'Pizza Palace'

# Access from parent
place = Place.objects.get(pk=r.pk)
print(place.restaurant.serves_pizza)  # True

3. Proxy Models

Same table, different Python behavior:

python
class Person(models.Model):
    first_name = models.CharField(max_length=50)
    last_name = models.CharField(max_length=50)
    is_staff = models.BooleanField(default=False)

    def get_full_name(self):
        return f'{self.first_name} {self.last_name}'


class StaffMember(Person):
    """No new table - uses Person table."""
    class Meta:
        proxy = True
        ordering = ['last_name', 'first_name']

    def get_full_name(self):
        return f'{self.last_name}, {self.first_name}'  # Different format

    objects = StaffManager()  # Custom manager

Use Cases for Proxy Models:

  • Different default ordering
  • Custom managers with filtered querysets
  • Different methods or properties
  • Different admin configurations

Choosing the Right Pattern

PatternUse WhenTrade-offs
AbstractSharing fields/methods, no common queriesNo polymorphic queries
Multi-tableNeed to query parent type, true polymorphismExtra JOINs, more complex
ProxySame data, different behaviorCan't add fields

Mixing Patterns

python
class Timestamp(models.Model):
    """Abstract mixin."""
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        abstract = True


class Content(Timestamp):
    """Concrete model with timestamps."""
    title = models.CharField(max_length=200)


class PublishedContent(Content):
    """Proxy with custom manager."""
    class Meta:
        proxy = True

    objects = PublishedManager()  # Only returns published items
```\n\n## Common Pitfalls\n\n1. **Not testing edge cases** — Always test model inheritance patterns with empty querysets, NULL values, and boundary conditions.\n2. **Premature optimization** — Profile queries with `.explain()` before applying complex optimizations.\n3. **Ignoring database-specific behavior** — Some model inheritance patterns features behave differently across PostgreSQL, MySQL, and SQLite.\n\n## Best Practices\n\n1. **Keep queries readable** — Use meaningful variable names and chain methods logically.\n2. **Test with realistic data** — Create fixtures that match production data patterns for accurate performance testing.\n3. **Document complex queries** — Add comments explaining the business logic behind non-obvious query patterns.\n\n## Summary\n\n- Model Inheritance Patterns is a core Django ORM feature for building efficient database queries.\n- Always consider query performance and use `.explain()` to verify query plans.\n- Test edge cases including empty results, NULL values, and large datasets.\n- Refer to the Django documentation for database-specific behavior and limitations.

Code Examples

python
class BaseContent(models.Model):
    """Abstract base - no table created."""
    title = models.CharField(max_length=200)
    slug = models.SlugField(unique=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    is_published = models.BooleanField(default=False)

    class Meta:
        abstract = True
        ordering = ['-created_at']

    def publish(self):
        self.is_published = True
        self.save()


class Article(Base
✓ Completed