Model Inheritance & the Meta API

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Django 6.1 (6.1.1 on PyPI; 6.1.2 is scheduled for 2026-10-06), Python 3.12-3.14 and PostgreSQL 15+, checked on 2026-10-03; where Django 5.2 LTS behaves differently the text says so. The listings were run in a scratch project on Python 3.13.16 and Django 6.1.1 with SQLite: manage.py check, makemigrations, sqlmigrate, migrate, a test suite of 13 tests (also run with -W error) and manage.py shell sessions all pass. The printed SQL, query counts and _meta output below are real output from that project. Where a feature needs a specific database the text says so; nothing on this page does.

Django models are Python classes, so they can inherit from each other, but a model is also a database table, and the interesting question is always the same: where do the inherited columns live? Django answers it in three ways — abstract base classes (the columns are copied into each child’s own table), multi-table inheritance (the columns stay in the parent’s table and the child’s table is joined to it) and proxy models (there is no new table at all). This page builds the inheritance hierarchy of the running Bookshelf example with each style, explains how Meta options are inherited in each case, lists the traps, and ends with the _meta API — the introspection interface the ORM, forms, admin and serializers use to discover what a model looks like. Fields themselves are covered in Models & Fields and the relations used here in Relationships; a concise account of when to prefer one style over another comes after the three style sections.

The three styles at a glance

The official documentation frames the choice as one decision: should the parent be a model in its own right, with its own table, or only a holder of common information that is visible through the children?

flowchart TD q{"Do you need the parent
to be queryable on its own?"} q -->|"No: it only holds shared
fields, methods, Meta"| abs["Abstract base class
abstract = True
no table for the parent"] q -->|"Yes: parent rows and
child rows both matter"| mt["Multi-table inheritance
one table per model
joined by an implicit OneToOneField"] q -->|"Same data, different
Python behaviour"| px["Proxy model
proxy = True
shares the parent's table"]
Table 1. Choosing an inheritance style
Style Tables Typical use Cost

Abstract base class

One per concrete child; the parent has none

Shared fields (created, modified), shared Meta, shared methods

None at run time; no way to query "all children" through the parent

Multi-table inheritance

One per model in the chain; child key is a OneToOneField to the parent

Extending a model you do not control (for example from another app)

A JOIN on every child query, extra INSERT/DELETE per save, a name taken from the reverse namespace

Proxy model

None added; same table as the concrete parent

Different default ordering or manager, extra methods, a separate admin entry or permission set

Cannot add fields; querying the parent never returns proxy instances

A fourth tool, mentioned only to separate it from proxies, is Meta.managed = False, covered under Proxy vs unmanaged models. Plain multiple inheritance with non-model Python mixins is covered under Multiple inheritance.

The scenario: Bookshelf models

Every example below belongs to one app, library, of the scratch project; the full module is about a hundred lines and each listing is a piece of it. Two things matter before reading on. First, the app label and class names appear in generated names (library_book, library_audiobook), so they are used literally in the SQL and in the reverse accessors. Second, none of the code needs a database other than SQLite, which makes it easy to try; all of it works the same on PostgreSQL 15+.

Abstract base classes

An abstract base class is a model with abstract = True in its Meta. Django does not create a table for it, does not give it a manager, and will not let you instantiate or save it; its only job is to be inherited from, at which point its fields are added to the child class as if you had typed them there.

TimeStampedModel

The canonical abstract model in Django projects is a pair of timestamps. It is not part of Django itself (it is a convention you write once in a shared app; the django-model-utils and django-extensions packages ship versions of it) and, with it written once, every model in the project gets the same two columns.

# library/models.py
from django.db import models


class TimeStampedModel(models.Model):
    """Abstract base: no table, no manager, only shared fields and Meta."""

    created = models.DateTimeField(auto_now_add=True)
    modified = models.DateTimeField(auto_now=True)

    class Meta:
        abstract = True
        ordering = ["-created"]
        get_latest_by = "created"


class Author(TimeStampedModel):
    name = models.CharField(max_length=100)

    def __str__(self):
        return self.name

makemigrations creates Author with the two inherited columns and nothing for TimeStampedModel, which the test suite confirms: the abstract class is flagged _meta.abstract, has no objects attribute, raises TypeError when called and has no table.

# library/tests.py (excerpt)
def test_abstract_has_no_table_or_manager(self):
    self.assertTrue(TimeStampedModel._meta.abstract)
    self.assertFalse(hasattr(TimeStampedModel, "objects"))
    with self.assertRaises(TypeError):
        TimeStampedModel()
    self.assertNotIn(
        "library_timestampedmodel", connection.introspection.table_names()
    )
Abstract models cannot be instantiated.

Three details of the two timestamp fields are worth knowing because they are the most common source of "my modified is wrong" tickets. auto_now_add sets the value once, when the row is created; auto_now sets it on every call to Model.save(); and neither is applied by QuerySet.update() or bulk_update(), which bypass save(). The fields are also editable=False, so they never appear in model forms or the admin form. The test below proves the save(update_fields=…​) behaviour: modified is only refreshed when the field is listed.

def test_auto_now_and_update(self):
    before = Book.objects.get(pk=self.book.pk).modified
    Book.objects.filter(pk=self.book.pk).update(title="X")
    self.assertEqual(Book.objects.get(pk=self.book.pk).modified, before)  # untouched

    b = Book.objects.get(pk=self.book.pk)
    b.save(update_fields=["title"])
    self.assertEqual(Book.objects.get(pk=self.book.pk).modified, before)  # not listed

    b.save(update_fields=["title", "modified"])
    self.assertGreater(Book.objects.get(pk=self.book.pk).modified, before)

The alternatives are to set the value yourself in the update (update(title="X", modified=timezone.now())) or to use a database-level default. Defining the default in the database is covered in Models & Fields (db_default, Django 5.0).

Meta inheritance

When Django builds an abstract base class it makes the Meta you declared available as an attribute. A child that declares no Meta of its own simply inherits the parent’s. A child that wants to add to it subclasses the parent’s Meta. Django adjusts one thing before installing the attribute: it sets abstract = False on the inherited Meta, so children of an abstract class do not become abstract by accident. To make an abstract class that inherits from another abstract class you set abstract = True on the child explicitly.

class Book(TimeStampedModel, OwnedModel):
    title = models.CharField(max_length=200)
    author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name="books")
    is_published = models.BooleanField(default=False)

    class Meta(TimeStampedModel.Meta):
        # extends the parent's Meta: keeps ordering and get_latest_by, adds more
        indexes = [models.Index(fields=["title"])]

The assertions in the test suite show the three outcomes: Author has no Meta and gets ordering as is; Book extends it and keeps ordering and get_latest_by; neither is abstract.

def test_meta_inheritance(self):
    self.assertEqual(Author._meta.ordering, ["-created"])      # inherited as-is
    self.assertEqual(Book._meta.ordering, ["-created"])        # via Meta(TimeStampedModel.Meta)
    self.assertEqual(Book._meta.get_latest_by, "created")
    self.assertFalse(Author._meta.abstract)                    # children are not abstract
    self.assertEqual(Author.objects.latest().name, "Ursula")   # latest() uses get_latest_by

Rules that follow from this, all from the official page:

  • Some options make no sense on an abstract parent. Setting db_table there would send every child that does not override it to the same table, which is almost certainly a bug.

  • A child that inherits from several abstract classes only gets the Meta of the first class listed (ordinary Python name resolution). To combine them, declare class Meta(First.Meta, Second.Meta) explicitly.

  • ordering on an abstract parent applies to every child, which is convenient and has a cost: an ORDER BY created DESC is added to every query that does not override it. Measure before putting ordering in a shared base class; the optimisation advice is in Query Optimization & Fetch Modes.

from django.db import models


class CommonInfo(models.Model):
    name = models.CharField(max_length=100)
    age = models.PositiveIntegerField()

    class Meta:
        abstract = True
        ordering = ["name"]


class Unmanaged(models.Model):
    class Meta:
        abstract = True
        managed = False


class Student(CommonInfo, Unmanaged):
    home_group = models.CharField(max_length=5)

    class Meta(CommonInfo.Meta, Unmanaged.Meta):
        pass

Reverse names must be unique per child

A ForeignKey or ManyToManyField declared on an abstract class is copied into each child with the same arguments. If related_name or related_query_name is a fixed string, two children then claim the same reverse accessor on the target model, and the system check refuses. The official fix is to put %(app_label)s and %(class)s into the value: %(class)s becomes the lowercased child class name and %(app_label)s the lowercased app label, so the reverse names differ per child and per app.

from django.conf import settings


class OwnedModel(models.Model):
    """Abstract base with a relation: the reverse name must be unique per child."""

    owner = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name="%(app_label)s_%(class)s_set",
        related_query_name="%(app_label)s_%(class)ss",
    )

    class Meta:
        abstract = True

With Book and Review both inheriting from OwnedModel, each user gets library_book_set and library_review_set, and the query names library_books and library_reviews work in filters:

def test_reverse_names_from_abstract(self):
    self.assertEqual(self.user.library_book_set.count(), 2)
    self.assertEqual(
        get_user_model().objects.filter(library_books__title="Draft").count(), 1
    )
    self.assertEqual(self.user.library_review_set.count(), 0)

Without the placeholders Django would report clashes. The test below builds that mistake in an isolated app registry (isolate_apps) and shows the two system-check errors, fields.E304 (reverse accessor) and fields.E305 (reverse query name):

@isolate_apps("library")
def test_fixed_related_name_in_abstract_clashes(self):
    class Person(models.Model):
        pass

    class Base(models.Model):
        owner = models.ForeignKey(Person, on_delete=models.CASCADE, related_name="same")

        class Meta:
            abstract = True

    class A(Base):
        pass

    class B(Base):
        pass

    ids = {e.id for e in A.check()} | {e.id for e in B.check()}
    self.assertEqual(ids, {"fields.E304", "fields.E305"})

If you omit related_name on an abstract field entirely, the default reverse name is the child’s name plus _set (for example book_set), exactly as if you had declared the field on the child directly; there is no clash as long as the child names are unique in the app.

Overriding or removing inherited fields

Fields inherited from an abstract base class may be overridden with another field, or removed by assigning None to the name; this restriction does not apply to concrete parents (see Field name "hiding" is not permitted).

class Plain(TimeStampedModel):
    class Meta(TimeStampedModel.Meta):
        abstract = True


class Note(Plain):
    modified = None  # removes the inherited field
    text = models.CharField(max_length=50)
def test_field_removed_with_none(self):
    names = [f.name for f in Note._meta.get_fields()]
    self.assertIn("created", names)
    self.assertNotIn("modified", names)

sqlmigrate confirms the table: CREATE TABLE "library_note" ("id" …​, "created" datetime NOT NULL, "text" varchar(50) NOT NULL), without modified.

One caution from the same page: model managers are inherited from abstract base classes too, so overriding a field that an inherited manager references can cause subtle bugs. Managers are covered in Managers & Custom QuerySets.

Multi-table inheritance

In multi-table inheritance every model in the hierarchy is a normal model: it has its own table, and both parent and child can be queried and created on their own. Django links each child to its parent with an automatically created OneToOneField. The scratch project extends the concrete Book with an AudioBook:

class AudioBook(Book):
    narrator = models.CharField(max_length=100)
    duration_minutes = models.PositiveIntegerField()

sqlmigrate shows what that costs in the database: a table that holds only the new columns, whose primary key is the foreign key to the parent’s table.

CREATE TABLE "library_audiobook" (
  "book_ptr_id" bigint NOT NULL PRIMARY KEY
      REFERENCES "library_book" ("id") DEFERRABLE INITIALLY DEFERRED,
  "narrator" varchar(100) NOT NULL,
  "duration_minutes" integer unsigned NOT NULL CHECK ("duration_minutes" >= 0)
);

The generated link field is equivalent to the declaration below; the official page shows exactly this form.

book_ptr = models.OneToOneField(
    Book,
    on_delete=models.CASCADE,
    parent_link=True,
    primary_key=True,
)

Querying and the JOIN

Querying the child joins to the parent; querying the parent never touches the child table. print(queryset.query) shows it (output reformatted over several lines here):

>>> print(AudioBook.objects.all().query)
SELECT "library_book"."id", "library_book"."created", "library_book"."modified",
       "library_book"."owner_id", "library_book"."title", "library_book"."author_id",
       "library_book"."is_published", "library_audiobook"."book_ptr_id",
       "library_audiobook"."narrator", "library_audiobook"."duration_minutes"
FROM "library_audiobook"
INNER JOIN "library_book" ON ("library_audiobook"."book_ptr_id" = "library_book"."id")
ORDER BY "library_book"."created" DESC

Notice that the ORDER BY uses the parent’s ordering: a child that does not specify ordering or get_latest_by inherits those two options from its parent. Writing ordering = [] on the child removes the inherited ordering.

Creating a child inserts two rows (one per table) and deleting works in both directions: deleting the parent row removes the child row through the CASCADE, and deleting the child instance removes the parent row too.

def test_multi_table(self):
    ab = AudioBook.objects.create(
        title="Audio", author=self.author, owner=self.user,
        narrator="Bea", duration_minutes=600,
    )
    self.assertEqual(ab.book_ptr_id, ab.pk)
    self.assertEqual(Book.objects.get(pk=ab.pk).audiobook, ab)
    with self.assertRaises(AudioBook.DoesNotExist):
        self.book.audiobook          # a plain Book has no child row
    with CaptureQueriesContext(connection) as ctx:
        list(AudioBook.objects.all())
    self.assertEqual(len(ctx), 1)
    self.assertIn("INNER JOIN", ctx[0]["sql"])
    Book.objects.filter(pk=ab.pk).delete()      # parent delete removes the child row
    self.assertFalse(AudioBook.objects.filter(pk=ab.pk).exists())

def test_multi_table_child_delete_removes_parent(self):
    ab = AudioBook.objects.create(
        title="Audio", author=self.author, owner=self.user,
        narrator="Bea", duration_minutes=60,
    )
    ab.delete()
    self.assertFalse(Book.objects.filter(pk=ab.pk).exists())

From the parent down to the child

The link also works downwards: the lowercased model name of the child is a reverse one-to-one attribute on the parent. If the parent row is not a child, the access raises RelatedObjectDoesNotExist, a subclass of the child’s DoesNotExist exception, which is why the test above catches AudioBook.DoesNotExist:

>>> Book.objects.get(pk=ab.pk).audiobook
<AudioBook: Left Hand>
>>> Book.objects.get(title="Dispossessed").audiobook
Traceback (most recent call last):
  ...
Book.audiobook.RelatedObjectDoesNotExist: Book has no audiobook.

This is the trap of multi-table inheritance in loops. Asking each parent row for its child is one query per row, the classic N+1 problem; the fix is select_related() with the child name. With the scratch data (two books):

>>> with CaptureQueriesContext(connection) as c:
...     for bk in Book.objects.all():
...         getattr(bk, "audiobook", None)
>>> len(c)
3
>>> with CaptureQueriesContext(connection) as c:
...     for bk in Book.objects.select_related("audiobook"):
...         getattr(bk, "audiobook", None)
>>> len(c)
1

A query for Book objects always returns Book instances, never AudioBook ones; Django has no built-in polymorphic queryset. Code that needs the specific type has to ask (via select_related, as above) or use a third-party package.

Meta and multi-table inheritance

A child in multi-table inheritance cannot extend its parent’s Meta: the options have already been applied to the parent’s table, and applying them again would only contradict them. So a child does not have access to its parent’s Meta at all, with the two exceptions given above (ordering and get_latest_by, inherited if the child sets none).

The implicit OneToOneField takes the default reverse name of the parent (audiobook), so a ForeignKey or ManyToManyField added to a child that points back at the parent clashes with it unless you give the field a related_name. The official page uses Place and Supplier; the check error from the scratch run is below.

class Place(models.Model):
    name = models.CharField(max_length=10)


class Supplier(Place):
    customers = models.ManyToManyField(Place)   # clashes with Supplier.place_ptr
fields.E305 Reverse query name for 'library.Supplier.customers' clashes with reverse
query name for 'library.Supplier.place_ptr'.
HINT: Add or change a related_name argument to the definition for
'library.Supplier.customers' or 'library.Supplier.place_ptr'.

The fix is models.ManyToManyField(Place, related_name="provider").

To control the name of the attribute that points back to the parent — and the name of the column — declare the OneToOneField yourself with parent_link=True. primary_key=True keeps the child table keyed by the parent’s key (the generated field has it; if you omit it the child gets its own id). The example also has a plain-Python mixin (see Multiple inheritance).

class CitationMixin:
    """Not a model: pure behaviour, no fields, no Meta."""

    def citation(self):
        return f"{self.title} ({self.created:%Y})"


class Anthology(CitationMixin, Book):
    base_book = models.OneToOneField(
        Book,
        on_delete=models.CASCADE,
        parent_link=True,
        primary_key=True,
        related_name="anthology_detail",
    )
    editor = models.CharField(max_length=100)

The migration creates base_book_id as the primary key and the parent’s reverse accessor is now anthology_detail:

CREATE TABLE "library_anthology" (
  "base_book_id" bigint NOT NULL PRIMARY KEY
      REFERENCES "library_book" ("id") DEFERRABLE INITIALLY DEFERRED,
  "editor" varchar(100) NOT NULL
);
def test_explicit_parent_link_and_mixin(self):
    an = Anthology.objects.create(title="Best of", author=author, owner=user, editor="E")
    self.assertEqual(an.base_book_id, an.pk)
    self.assertEqual(Book.objects.get(pk=an.pk).anthology_detail, an)
    self.assertTrue(an.citation().startswith("Best of ("))

Proxy models

A proxy model is a Python-level view of an existing model’s table. You declare it like any model and set proxy = True. Creating, updating and deleting through the proxy touches the same rows as the parent; the difference is that the proxy can change default ordering or the default manager and add methods without altering the parent.

class PublishedManager(models.Manager):
    def get_queryset(self):
        return super().get_queryset().filter(is_published=True)


class PublishedBook(Book):
    objects = PublishedManager()

    class Meta:
        proxy = True
        ordering = ["title"]
        verbose_name = "published book"

    def publish_label(self):
        return f"{self.title} by {self.author.name}"

makemigrations records Create proxy model PublishedBook and sqlmigrate emits -- (no-op) for it: no table, no column, no data copy. The generated SQL for the proxy shows the manager’s WHERE and the proxy’s own ordering, against the parent’s table:

>>> print(PublishedBook.objects.all().query)
SELECT "library_book"."id", "library_book"."created", "library_book"."modified",
       "library_book"."owner_id", "library_book"."title", "library_book"."author_id",
       "library_book"."is_published"
FROM "library_book" WHERE "library_book"."is_published" = True
ORDER BY "library_book"."title" ASC

Querysets return the model you asked for

There is no way to make Book queries return PublishedBook objects: the whole point of a proxy is that code relying on Book keeps getting Book, and your own code opts in to the extension. A proxy and its parent share rows, so a change made through one is visible through the other:

def test_proxy(self):
    self.assertEqual(PublishedBook.objects.count(), 1)
    self.assertEqual(Book.objects.count(), 2)
    pb = PublishedBook.objects.get()
    self.assertIsInstance(pb, PublishedBook)
    self.assertEqual(pb.publish_label(), "Dispossessed by Ursula")
    self.assertIsInstance(Book.objects.get(pk=pb.pk), Book)
    self.assertNotIsInstance(Book.objects.get(pk=pb.pk), PublishedBook)
    self.assertIs(PublishedBook._meta.concrete_model, Book)

Restrictions and managers

  • A proxy must inherit from exactly one non-abstract model class (there is no row connection between several tables). It may also inherit from any number of abstract classes provided they define no fields, and from any number of proxies that share a non-abstract parent.

  • A proxy cannot declare model fields. The system check reports models.E017 Proxy model 'S' contains model fields.

  • With no managers declared, the proxy inherits its parent’s managers. A manager declared on the proxy becomes the default one, while the parent’s managers stay available. To add a manager without replacing the default, put the new manager in an abstract class and inherit it after the primary parent: class MyPerson(Person, ExtraManagers).

Permissions, content types and the admin

Proxy models are the usual way to get a second admin entry or a separate permission set for the same table. Django’s permissions are keyed by content type, and ContentType.objects.get_for_model() has a for_concrete_model argument (default True), so by default a proxy resolves to the parent’s content type, and passing False gives the proxy its own. The default permissions (add_, change_, delete_, view_) are created for proxy models too; the scratch project, after migrate, has view_publishedbook next to view_book.

# default: the proxy shares the concrete model's content type ...
ContentType.objects.get_for_model(PublishedBook) == ContentType.objects.get_for_model(Book)   # True
# ... unless you ask for the proxy's own
ContentType.objects.get_for_model(PublishedBook, for_concrete_model=False) == \
    ContentType.objects.get_for_model(Book)                                                   # False

A model admin for the proxy lets editors with only view_publishedbook/change_publishedbook work on published books without touching the rest of the catalogue; the admin is covered in The Admin Site.

Proxy vs unmanaged models

A proxy looks similar to an unmanaged model (Meta.managed = False) pointed at the same db_table, but the two differ in kind. An unmanaged model is its own class with its own copy of the field definitions, so you must keep the two declarations synchronised; a proxy directly inherits fields and managers and cannot drift. The official rule of thumb:

  • mirroring an existing table or a database view, perhaps with only some of its columns: managed = False;

  • changing only the Python behaviour while keeping the same fields: proxy = True.

class BookTitleRow(models.Model):
    title = models.CharField(max_length=200)

    class Meta:
        managed = False
        db_table = "library_book"

sqlmigrate for it prints -- (no-op): Django creates and drops nothing for an unmanaged model, so the table must exist already (for instance a database view or a table owned by another system). inspectdb produces models like this one from an existing schema; see Transactions, Raw SQL & Multiple Databases.

Multiple inheritance

A model may inherit from several parents and normal Python name resolution applies: the first base that defines a name (for example Meta) wins, and the other Meta classes are ignored. The page’s advice is to avoid it except for mixins, classes that add a field or a method to every class that includes them. A mixin that only adds behaviour does not need to be a model at all; CitationMixin above is a plain Python class and needs no Meta, no migration and no registry entry.

Two models that both declare the default id primary key cannot be combined in one child; the error is avoided by naming the keys (article_id = models.AutoField(primary_key=True) in each parent) or by inheriting both from a common ancestor with explicit OneToOneField(…​, parent_link=True) links, as in the official examples.

Field name "hiding" is not permitted

Plain Python lets a subclass override any attribute, but Django does not allow it for fields of concrete models: if a non-abstract parent has a field author, no model that inherits from it can declare a field or an attribute of that name, and Django raises FieldError. The restriction applies to the attribute name Python sees, not to the column: in multi-table inheritance you can use the same db_column in child and ancestor because they live in different tables. It applies to the extra attributes some fields create as well (the _id attribute of a ForeignKey, the reverse accessors), which cannot be overridden unless the defining field is changed or removed. It does not apply to ordinary non-field attributes (methods, constants), which can be overridden freely, nor to fields inherited from an abstract base. Fields inherited from several abstract parents are resolved depth-first, unlike Python’s breadth-first MRO for diamonds; this only matters for hierarchies you should avoid anyway.

class P(models.Model):
    author = models.CharField(max_length=3)

class C(P):
    author = models.CharField(max_length=5)
FieldError: Local field 'author' in class 'C' clashes with field of the same name from base class 'P'.

Trade-offs and recommendations

Situation Prefer

Shared created/modified, owner, soft-delete flag, common Meta

Abstract base class (TimeStampedModel). Zero run-time cost.

Several kinds of "thing" with a few type-specific columns (AudioBook)

Often a single table with a choices field and nullable columns, or a separate model with a OneToOneField you control, rather than multi-table inheritance.

Extending a model from a third-party app

Multi-table inheritance, or a OneToOneField "profile" model; avoid editing the third-party model.

Another default ordering, manager, admin entry or permission set on existing data

Proxy model

Read-only mapping of a legacy table or view

managed = False

Multi-table inheritance is the one to be careful with. It is easy to write and tempting to read as "Python inheritance for tables", but every query for the child is a JOIN, every save is two writes, delete() crosses two tables, and the parent gets a reverse accessor named after the child; polymorphic queries are not built in. Many Django guides, including the Two Scoops of Django book consulted for this section, recommend abstract base classes (and OneToOneField or foreign keys) over multi-table inheritance for those reasons. That is a design guideline, not a Django rule; the example above is useful because it shows exactly what the ORM does.

The Meta options and inheritance, summarised

Table 2. How Meta behaves per style
Abstract base Multi-table child Proxy

Is Meta inherited?

Yes, if the child declares none; extend with class Meta(Parent.Meta)

No, except ordering and get_latest_by when the child sets none

Same rules as a normal model: the options are inherited the same way as for regular models (the official page says proxies "inherit Meta attributes in the same way as regular models")

abstract

Reset to False on the inherited Meta; set True explicitly to chain abstract classes

n/a

n/a

db_table

Do not set on the parent

Own table

Shares the parent’s table

Several parents with Meta

First one only unless combined explicitly

First one only

Exactly one concrete parent

The model _meta API

Every model class has a _meta attribute, an instance of django.db.models.options.Options. The leading underscore is historical: the official documentation calls the _meta API "at the core of the Django ORM" and documents it as public. Lookups, queries, forms, the admin, serializers and dumpdata use it to learn what fields a model has and how they relate to other models. It is the right tool for writing generic code, such as a CSV exporter or a "show all fields" admin helper, and the wrong one for application logic that could simply name the fields it uses.

The documented API of the Options class has three entry points: get_field(), get_fields() and pk_fields. Everything else is built from them plus a set of boolean flags on each field.

get_field

Options.get_field(field_name) returns the field instance for a name. The name may be a field on the model, one inherited from an abstract parent or a concrete one, or the reverse of a relation defined elsewhere (in order of preference the related_query_name, the related_name, or the automatically generated name). It raises FieldDoesNotExist if the name is unknown, and hidden fields (hidden=True) cannot be retrieved by name.

>>> Book._meta.get_field("author")
<django.db.models.fields.related.ForeignKey: author>
>>> Author._meta.get_field("books")          # reverse side, found via related_name
<ManyToOneRel: library.book>
>>> Book._meta.get_field("nope")
Traceback (most recent call last):
    ...
django.core.exceptions.FieldDoesNotExist: Book has no field named 'nope'

get_fields

Options.get_fields(include_parents=True, include_hidden=False) returns a tuple of all field instances, including reverse relations (the objects a related_name exposes) and, with include_hidden=True, hidden ones, for example the reverse side of a ForeignKey whose related_name starts with ` (in the scratch project it is the automatic through-table's `Book_tags). include_parents=False keeps only fields declared directly on the model; fields from models that directly inherit from abstract models or proxy classes still count as local.

The scratch project shows all of the inheritance styles at once. Reverse relations (here reviews and audiobook) come first, forward fields follow in declaration order, and the multi-table child shows the inherited columns plus its parent link:

>>> [f.name for f in Book._meta.get_fields()]
['reviews', 'audiobook', 'id', 'created', 'modified', 'owner', 'title', 'author', 'is_published', 'tags']
>>> [f.name for f in AudioBook._meta.get_fields()]
['reviews', 'id', 'created', 'modified', 'owner', 'title', 'author', 'is_published', 'tags',
 'book_ptr', 'narrator', 'duration_minutes']
>>> [f.name for f in AudioBook._meta.get_fields(include_parents=False)]
['book_ptr', 'narrator', 'duration_minutes']
>>> [f.name for f in PublishedBook._meta.get_fields()]     # a proxy sees the same fields
['reviews', 'audiobook', 'id', 'created', 'modified', 'owner', 'title', 'author', 'is_published', 'tags']
>>> sorted(set(f.name for f in Book._meta.get_fields(include_hidden=True))
...        - set(f.name for f in Book._meta.get_fields()))
['Book_tags+']

pk_fields

Options.pk_fields is a list of the fields composing the primary key. For an ordinary model it contains the one primary-key field; when a CompositePrimaryKey is declared it contains every field that key references. Because of composite keys, code that reads _meta.pk to get the primary key field should use pk_fields when it must support composite keys. Composite primary keys appeared in Django 5.2; they are covered in Constraints, Indexes & Composite Keys.

Inheritance shows up here as well: the primary key of a multi-table child is its parent link.

>>> Book._meta.pk_fields
[<django.db.models.fields.BigAutoField: id>]
>>> AudioBook._meta.pk_fields
[<django.db.models.fields.related.OneToOneField: book_ptr>]

Field flags: telling fields apart without isinstance

get_fields() returns forward fields (CharField, ForeignKey…​) and reverse relation objects (ManyToOneRel…​) side by side. The official "Field attribute reference" lists the attributes meant for narrowing a search, and tells you to use them instead of isinstance checks. They are:

Table 3. Field attributes used with the _meta API
Attribute Meaning

auto_created

The field was created automatically, for example the OneToOneField of model inheritance

concrete

The field has a database column

hidden

The field is not returned by get_fields() by default

is_relation

The field refers to one or more other models

model

The model on which the field is defined: for an inherited field this is the parent, not the child

many_to_many, many_to_one, one_to_many, one_to_one

Cardinality flags; booleans only when is_relation is true, otherwise None

related_model

The model the field points to (None for a GenericForeignKey)

An introspection helper

The documented API is enough to write a model describer that works for any model, abstract-derived, multi-table or proxy, forward or reverse:

# library/introspection.py
from django.apps import apps


def describe(model):
    """Print every field of *model* using only the documented _meta API."""
    pk = ", ".join(f.name for f in model._meta.pk_fields)
    print(f"{model._meta.label}  (pk: {pk})")
    for field in model._meta.get_fields():
        if not field.is_relation:
            kind = "column" if field.concrete else "virtual"
            target = ""
        elif field.many_to_many:
            kind, target = "m2m", f" -> {field.related_model._meta.label}"
        elif field.many_to_one:
            kind, target = "fk", f" -> {field.related_model._meta.label}"
        elif field.one_to_one:
            kind, target = "o2o", f" -> {field.related_model._meta.label}"
        else:  # one_to_many: reverse side of a ForeignKey
            kind, target = "reverse", f" <- {field.related_model._meta.label}"
        auto = " [auto]" if field.auto_created else ""
        print(f"  {field.name:<18} {kind:<8}{target}{auto}")


def all_models_with_fk_to(target):
    """Which models declare a ForeignKey to *target*? (walks the app registry)"""
    return sorted(
        f"{m._meta.label}.{f.name}"
        for m in apps.get_models()
        for f in m._meta.get_fields(include_parents=False)
        if f.is_relation and f.many_to_one and f.related_model is target
    )
>>> describe(AudioBook)
library.AudioBook  (pk: book_ptr)
  reviews            reverse  <- library.Review [auto]
  id                 column   [auto]
  created            column
  modified           column
  owner              fk       -> auth.User
  title              column
  author             fk       -> library.Author
  is_published       column
  tags               m2m      -> library.Tag
  book_ptr           o2o      -> library.Book [auto]
  narrator           column
  duration_minutes   column
>>> all_models_with_fk_to(Author)
['library.Book.author', 'library.PublishedBook.author']

Two details from that run: include_parents=False keeps AudioBook and Anthology out of the second list because their author field is inherited from Book, and the proxy PublishedBook shows up because Django counts the fields of a proxy as local (the rule quoted above).

The Meta options mirrored on _meta

The options you set in a model’s inner Meta class are also readable on _meta, which is what generic code uses. A few that the scratch session printed for Book: Book._meta.label is library.Book (and label_lower library.book), app_label library, model_name book, db_table library_book, verbose_name book, ordering ['-created'], abstract False, proxy False; for the proxy, PublishedBook._meta.proxy is True, db_table is the parent’s library_book and concrete_model is Book. _meta.parents maps each parent model to the link field (for AudioBook, {Book: book_ptr}). The options themselves are documented in the Meta options reference; the surrounding attributes (label, model_name, concrete_model, parents) are implementation attributes that are widely used but not individually documented, so prefer the documented methods above in code you intend to keep across upgrades.

Typical uses

  • Generic admin and export code: walk get_fields() and read verbose_name and value_from_object().

  • Finding the reverse relations of a model before deleting it, or building a "related objects" report: filter for one_to_many or one_to_one fields with auto_created=True and concrete=False.

  • Checking that a name is a real field before using it in order_by() or only() from user input: wrap get_field() in try/except FieldDoesNotExist and refuse non-concrete or relation fields. This is also an allow-list approach to avoid exposing columns that should stay private; never pass raw user input as a field name without it.

  • Tests: assert that a base class really contributes the fields you think it does, as the Note test above does.

_meta runs at import time and during queries, but it caches heavily; the cost is in the first call per model, after which get_fields() returns cached tuples (they are recomputed when apps are loaded or models change).

Request cycle, async and tooling notes

Model classes are built when Django imports models.py during django.setup(), so every inheritance rule above is applied at start-up, not per request: a bad related_name fails manage.py check or runserver, never a request. The only per-request costs of inheritance are the JOIN and double write of multi-table models and the ORDER BY from an inherited ordering. Instances of abstract-derived, multi-table and proxy models behave identically in sync and async code; accessing a reverse one-to-one such as book.audiobook is a database query, so in an async view use select_related() up front or await Book.objects.select_related("audiobook").aget(pk=…​) rather than touching the attribute lazily. Migrations record proxy models too (proxy=True in CreateModel, no SQL), which is why you will see a migration file for a change that creates no table; on PostgreSQL the same models produce the same tables, and the scratch SQL above differs only in column types.

Where the books are outdated

  • Two Scoops of Django 1.11 recommends TimeStampedModel as an abstract base class and warns against multi-table inheritance; the advice stands. Listings of that era that put index_together in a shared Meta no longer work: it was removed in Django 5.1, so use Meta.indexes and Meta.constraints (see Constraints, Indexes & Composite Keys).

  • Projects of that era also assumed an AutoField implicit primary key; since Django 6.0 the default is BigAutoField, which matters for the OneToOneField parent link and for foreign keys to inherited models.

  • Older posts and books use the pre-1.8 _meta API (get_all_field_names(), get_field_by_name(), get_m2m_with_model()). It was reworked in Django 1.8 and the old methods were removed; use get_fields(), get_field() and the field flags above.

  • Managers: modern code can rely on managers being inherited with ordinary Python name resolution, and Meta.default_manager_name selects the default one when that is not what you want; see Managers & Custom QuerySets.

What changed recently

  • Django 5.2 (2025): CompositePrimaryKey and with it Options.pk_fields; code that assumed a single primary key field via _meta.pk has to handle several fields.

  • Django 5.1: Meta.index_together was removed after a long deprecation; use Meta.indexes. Any shared abstract base that still declares it fails.

  • Django 5.0: db_default, which is a better home for a created default than a Python callable when the default must also apply to raw SQL inserts (see Models & Fields).

  • Django 6.0: BigAutoField became the default DEFAULT_AUTO_FIELD; abstract bases do not pin a type, so the implicit id follows the setting, which is why the migration here creates bigint foreign keys to it.

  • Django 6.1: nothing in the inheritance rules or the _meta API changed; the 6.1 features that touch models (DB_CASCADE, DB_SET_NULL, DB_SET_DEFAULT for on_delete, and fetch modes) apply to inherited relations in the same way as to any other; see Relationships and Query Optimization & Fetch Modes.