DEV Community

Cover image for author.book_set? Reverse Relationships in Django, Finally Clear
Anas Issath
Anas Issath

Posted on Originally published at Medium

author.book_set? Reverse Relationships in Django, Finally Clear

Where that mysterious _set comes from, what related_name actually does, and how to query backwards across a ForeignKey.

You define a ForeignKey from Book to Author, and book.author works exactly as you'd expect: a book points to its author. But then you want to go the other way. Given an author, get all their books. You reach for author.books... and it crashes.

You dig around and discover the answer is author.book_set.all(), and now you have questions. Where did book_set come from? Why _set? And why does some code use author.books instead?

This is one of the most quietly confusing corners of Django, and it trips up beginners and experienced developers alike. The good news: it's governed by a couple of simple rules. Once you see them, reverse relationships become one of the most useful things the ORM does for you. You get the "backwards" direction of every relationship for free, and you can even filter across it.

Let's make it completely clear. Everything here is current for Django 6.x.


Forward Is Easy: Following the ForeignKey You Defined

Start with the direction everyone understands. You put the ForeignKey on Book:

from django.db import models

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

class Book(models.Model):
    title = models.CharField(max_length=100)
    author = models.ForeignKey(Author, on_delete=models.CASCADE)
Enter fullscreen mode Exit fullscreen mode

The ForeignKey lives on Book, pointing at Author. So from a book, reaching its author is natural. The field is right there on the model:

book.author          # → the Author object. Easy.
book.author.name
Enter fullscreen mode Exit fullscreen mode

This is forward access: following the relationship in the direction you declared it, from the model that has the ForeignKey to the model it points at. No surprises. The interesting part is the other direction.


Reverse Is the Gift: Following the Relationship Backwards

Here's the thing Django does for you that people don't appreciate: when you define a ForeignKey, you automatically get the reverse direction too, for free.

You defined Book → Author, and without writing a single extra line, Django also gives Author a way to find all its books. You never declared anything on Author, yet the backward path exists.

The only question is what that backward accessor is called. By default, Django builds the name with a specific formula:

<lowercase model name>_set
Enter fullscreen mode Exit fullscreen mode

Your model is Book, so the reverse accessor on an author is book_set:

author.book_set.all()        # → all books by this author
author.book_set.filter(title__startswith="The")
Enter fullscreen mode Exit fullscreen mode

That's where the mysterious _set comes from. It's not magic, it's a naming convention: the lowercase name of the model that has the ForeignKey, plus _set. The _set suffix is Django's way of signaling "this is a set of related objects," since one author has many books.

And crucially, book_set isn't a plain list. It's a full related manager, so you can .all(), .filter(), .count(), .order_by() on it just like Book.objects. The reverse direction gives you a complete queryable manager scoped to that one author's books.


related_name: Giving the Reverse Accessor a Better Name

author.book_set works, but it's clunky, and for some relationships the default name is downright ugly or ambiguous. So Django lets you rename the reverse accessor with the related_name argument on the ForeignKey:

class Book(models.Model):
    title = models.CharField(max_length=100)
    author = models.ForeignKey(
        Author,
        on_delete=models.CASCADE,
        related_name="books",        # ← name the reverse accessor
    )
Enter fullscreen mode Exit fullscreen mode

Now the reverse accessor is called exactly what you'd naturally expect:

author.books.all()           # instead of author.book_set.all()
author.books.filter(title__startswith="The")
Enter fullscreen mode Exit fullscreen mode

This is why some code uses author.books and some uses author.book_set. It depends entirely on whether the ForeignKey set a related_name. The rule is simple:

No related_name → the default author.book_set
related_name="books" → author.books

Setting related_name is considered good practice precisely because author.books reads far better than author.book_set, and because it makes the reverse relationship explicit and intentional rather than relying on an auto-generated name.


The Case Where related_name Isn't Optional: Two FKs to the Same Model

There's one situation where related_name goes from "nice to have" to mandatory, and it's a common one. Suppose a Book has two ForeignKeys to Author, a primary author and an editor:

class Book(models.Model):
    author = models.ForeignKey(Author, on_delete=models.CASCADE)
    editor = models.ForeignKey(Author, on_delete=models.CASCADE)   # 💥
Enter fullscreen mode Exit fullscreen mode

Django will refuse to start, raising an error about a reverse accessor clash. Why? Because both ForeignKeys point at Author, so both would try to create the same default reverse accessor (book_set) on Author. Django can't have two different relationships answering to the same name, so it errors out and tells you to disambiguate.

The fix is to give each one its own related_name:

class Book(models.Model):
    author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name="authored_books")
    editor = models.ForeignKey(Author, on_delete=models.CASCADE, related_name="edited_books")
Enter fullscreen mode Exit fullscreen mode

Now an author object has both author.authored_books.all() and author.edited_books.all(), cleanly separated.

Any time a model has more than one ForeignKey to the same target, related_name is required. This clash is one of the most common "why won't my Django app even start?" errors new developers hit.


The Real Power: Querying Across the Reverse Relationship

Here's where reverse relationships go from convenient to genuinely powerful. You can filter one model based on its related objects on the other side, querying backwards across the relationship using the double-underscore syntax.

"Find all authors who have written a book titled 'Dune'":

# Starting from Author, filter by a field on the related Book
Author.objects.filter(book__title="Dune")
Enter fullscreen mode Exit fullscreen mode

Read book__title as "follow the reverse relation to Book, then look at its title." This lets you ask questions that span the relationship in a single query (authors who've published since 2020, authors with no books at all, authors whose books are all out of print) without ever loading objects into Python and looping.

The name you use in that filter (book__...) is controlled by a second, lesser-known argument: related_query_name. By default it's the lowercase model name (book), which is why book__title works above. If you want the filter name to differ from the accessor name, you set it explicitly:

author = models.ForeignKey(
    Author,
    on_delete=models.CASCADE,
    related_name="books",             # for ACCESS: author.books.all()
    related_query_name="book",        # for FILTERS: filter(book__title=...)
)
Enter fullscreen mode Exit fullscreen mode

The distinction that trips people up: related_name is for accessing related objects (author.books), while related_query_name is for filtering across the relation (filter(book__...)).

They often look similar, but they control two different things: one is the attribute you read, the other is the keyword you filter on. If you don't set related_query_name, Django uses related_name for filtering too, or the model name if neither is set.


One More: Turning the Reverse Relationship Off

Occasionally you have a ForeignKey where the backward direction is genuinely useless. Think of a logging table pointing at User, where you'd never ask "give me all logs for this user" through the ORM accessor and you don't want to clutter User with it.

You can disable the reverse accessor entirely with related_name="+":

class Log(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="+")
Enter fullscreen mode Exit fullscreen mode

Now user.log_set doesn't exist. The forward direction (log.user) still works, but there's no reverse accessor at all.

This is a niche tool, but it's handy for avoiding accessor clashes on models you reference from many places, and for keeping heavily-referenced models like User from accumulating dozens of reverse managers you'll never use.


Why Understanding This Makes You Better at Django

Reverse relationships are where a lot of developers' mental model of the ORM is fuzziest. They memorize "it's book_set or sometimes the related_name" without really grasping that every ForeignKey is automatically bidirectional, and that the backward direction is a full, queryable manager you can filter through.

That fuzziness costs them. They load an author's books with a loop and a manual filter instead of author.books.all(). Or they pull every author into Python to find the ones with recent books instead of Author.objects.filter(book__published__year=2026).

The developer who gets it sees relationships as two-way streets and queries them fluently in both directions, letting the database do the work.

And the rules are genuinely few:

  • A ForeignKey gives you forward access (book.author) and a free reverse accessor.
  • The reverse accessor is named <model>_set by default, or whatever you set with related_name (author.books).
  • related_name is required when two FKs point at the same model.
  • You query backwards across the relation with the double-underscore syntax (filter(book__title=...)), whose name is governed by related_query_name.
  • related_name="+" turns the reverse direction off.

Hold those, and author.book_set stops being a mystery you look up every time and becomes one half of a relationship you can navigate in either direction without thinking.


The 30-Second Takeaway

  • A ForeignKey gives you both directions for free: forward (book.author, following the FK you declared) and a reverse accessor on the other model, created automatically.
  • The default reverse accessor is <lowercase model>_set, so Book's FK to Author gives you author.book_set. That's where the mysterious _set comes from. It's a full related manager (.all(), .filter(), .count()), not a plain list.
  • related_name renames that accessor to something readable: related_name="books" → author.books.all(). This is why some code says author.books and some says author.book_set.
  • related_name is mandatory when a model has two FKs to the same target. Otherwise both try to create book_set and Django refuses to start (the "reverse accessor clash" error). Give each its own name.
  • Query backwards across the relation with double underscores: Author.objects.filter(book__title="Dune") finds authors by a field on their related books. One query, no Python loop.
  • related_query_name controls the name used in those filters (book__...), which is separate from related_name (used for access, author.books). Access vs filter: two different settings.
  • related_name="+" disables the reverse accessor entirely, which is useful for heavily-referenced models like User where the backward direction would just be clutter.

Your Turn

Pick any ForeignKey in your models and ask: can I name its reverse accessor? Adding related_name="books" turns an ugly author.book_set into a clean author.books everywhere, and makes every "get all the related things" query read like plain English.

Do you always set related_name, or rely on the _set default? Tell me in the comments 👇


If this was useful, follow me here for more stories from the backend trenches, and subscribe to The Backend Tech Lead on Substack for weekly blueprints.

Top comments (0)