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?
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"]
| Style | Tables | Typical use | Cost |
|---|---|---|---|
Abstract base class |
One per concrete child; the parent has none |
Shared fields ( |
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 |
Extending a model you do not control (for example from another app) |
A |
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)
Source: https://docs.djangoproject.com/en/6.1/ref/models/fields/#django.db.models.DateField.auto_now
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_tablethere 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
Metaof the first class listed (ordinary Python name resolution). To combine them, declareclass Meta(First.Meta, Second.Meta)explicitly. -
orderingon an abstract parent applies to every child, which is convenient and has a cost: anORDER BY created DESCis added to every query that does not override it. Measure before puttingorderingin 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).
Reverse relations and the parent link name
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").
Naming the parent link
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 |
Abstract base class ( |
Several kinds of "thing" with a few type-specific columns ( |
Often a single table with a |
Extending a model from a third-party app |
Multi-table inheritance, or a |
Another default ordering, manager, admin entry or permission set on existing data |
Proxy model |
Read-only mapping of a legacy table or view |
|
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
| Abstract base | Multi-table child | Proxy | |
|---|---|---|---|
Is |
Yes, if the child declares none; extend with |
No, except |
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") |
|
Reset to |
n/a |
n/a |
|
Do not set on the parent |
Own table |
Shares the parent’s table |
Several parents with |
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:
| Attribute | Meaning |
|---|---|
|
The field was created automatically, for example the |
|
The field has a database column |
|
The field is not returned by |
|
The field refers to one or more other models |
|
The model on which the field is defined: for an inherited field this is the parent, not the child |
|
Cardinality flags; booleans only when |
|
The model the field points to ( |
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']
Source: https://docs.djangoproject.com/en/6.1/ref/models/meta/ and https://docs.djangoproject.com/en/6.1/ref/applications/#django.apps.apps.get_models
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 readverbose_nameandvalue_from_object(). -
Finding the reverse relations of a model before deleting it, or building a "related objects" report: filter for
one_to_manyorone_to_onefields withauto_created=Trueandconcrete=False. -
Checking that a name is a real field before using it in
order_by()oronly()from user input: wrapget_field()intry/except FieldDoesNotExistand 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
Notetest 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
TimeStampedModelas an abstract base class and warns against multi-table inheritance; the advice stands. Listings of that era that putindex_togetherin a sharedMetano longer work: it was removed in Django 5.1, so useMeta.indexesandMeta.constraints(see Constraints, Indexes & Composite Keys). -
Projects of that era also assumed an
AutoFieldimplicit primary key; since Django 6.0 the default isBigAutoField, which matters for theOneToOneFieldparent link and for foreign keys to inherited models. -
Older posts and books use the pre-1.8
_metaAPI (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; useget_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_nameselects the default one when that is not what you want; see Managers & Custom QuerySets.
What changed recently
-
Django 5.2 (2025):
CompositePrimaryKeyand with itOptions.pk_fields; code that assumed a single primary key field via_meta.pkhas to handle several fields. -
Django 5.1:
Meta.index_togetherwas removed after a long deprecation; useMeta.indexes. Any shared abstract base that still declares it fails. -
Django 5.0:
db_default, which is a better home for acreateddefault than a Python callable when the default must also apply to raw SQL inserts (see Models & Fields). -
Django 6.0:
BigAutoFieldbecame the defaultDEFAULT_AUTO_FIELD; abstract bases do not pin a type, so the implicitidfollows the setting, which is why the migration here createsbigintforeign keys to it. -
Django 6.1: nothing in the inheritance rules or the
_metaAPI changed; the 6.1 features that touch models (DB_CASCADE,DB_SET_NULL,DB_SET_DEFAULTforon_delete, and fetch modes) apply to inherited relations in the same way as to any other; see Relationships and Query Optimization & Fetch Modes.