Django

How to Correctly Filter on Django's Related Managers

Tired of confusing Django queries? Learn how to correctly filter your data, from basic .filter() and field lookups to advanced Q objects for complex logic.

D

Daniel Carter

A senior full-stack developer with over a decade of experience building with Django.

6 min read18 views

If you're building any kind of data-driven application with Django, you'll spend a huge amount of your time fetching data. But you rarely want all the data. You want specific pieces: the published blog posts, the active users, the products under $50. This is where Django's filtering capabilities shine, and mastering them is a fundamental step toward becoming a proficient Django developer.

The Django ORM (Object-Relational Mapper) provides a powerful and intuitive API for interacting with your database. At the heart of this API is the QuerySet, and the most common way to shape a QuerySet is by filtering it. In this guide, we'll walk through everything you need to know, from the absolute basics to complex, multi-conditional queries.

Our Example Model: A Simple Blog Post

To make our examples concrete, let's assume we're working with a simple Post model for a blog application. All our queries will be based on this model.

# models.py
from django.db import models
from django.contrib.auth.models import User

class Post(models.Model):
    STATUS_CHOICES = (
        ('draft', 'Draft'),
        ('published', 'Published'),
    )
    title = models.CharField(max_length=200)
    content = models.TextField()
    author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='blog_posts')
    published_date = models.DateTimeField(null=True, blank=True)
    status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='draft')

    def __str__(self):
        return self.title

The Foundation: .filter() and .exclude()

The two most fundamental methods for filtering are .filter() and .exclude(). They do exactly what their names suggest.

.filter(**kwargs) returns a new QuerySet containing objects that match the given lookup parameters.

For example, to get all posts that have been published, you would write:

# Get all posts with the status 'published'
published_posts = Post.objects.filter(status='published')

.exclude(**kwargs) returns a new QuerySet containing objects that do not match the given lookup parameters.

If you wanted to get all posts that are not drafts, you could do this:

# Get all posts that are not in 'draft' status
not_drafts = Post.objects.exclude(status='draft')

It's important to remember that both .filter() and .exclude() return a QuerySet, not a list of objects. This is a key feature we'll explore next. The database query isn't actually executed until the QuerySet is evaluated (e.g., by looping over it).

Chaining Filters for More Specificity

Because filtering methods return QuerySets, you can chain them together to build more specific queries. Each subsequent filter narrows down the results from the previous one. This is effectively a database AND clause.

Let's say you want to find all published posts written by a specific user.

from django.contrib.auth.models import User

# First, get the user
try:
    some_user = User.objects.get(username='daniel')
except User.DoesNotExist:
    some_user = None

if some_user:
    # Chain filters to find published posts by this user
    user_posts = Post.objects.filter(author=some_user).filter(status='published')

This is cleaner and often more readable than trying to cram everything into one call. However, it's functionally identical to passing multiple arguments to a single .filter() call:

Advertisement
# This does the exact same thing
user_posts = Post.objects.filter(author=some_user, status='published')

Both versions translate to a SQL query with a WHERE ... AND ... clause.

Unlocking Precision with Field Lookups (The Double Underscore `__`)

So far, we've only checked for exact matches (status='published'). But what if you need more nuance? Django's "field lookups" are the answer. They are specified by adding a double underscore (__) after the field name.

Here are some of the most common and useful lookups:

Text Matching Lookups

  • icontains: Case-insensitive "contains". Finds objects where the field contains the given text.
  • startswith / endswith: Checks if a value starts or ends with the given string (case-sensitive).
  • istartswith / iendswith: The case-insensitive versions.
# Find all posts with 'Django' anywhere in the title (case-insensitive)
django_posts = Post.objects.filter(title__icontains='django')

# Find posts whose titles start with 'How to'
how_to_posts = Post.objects.filter(title__startswith='How to')

Comparison Lookups

  • gt: Greater than.
  • gte: Greater than or equal to.
  • lt: Less than.
  • lte: Less than or equal to.
  • in: Member of a list.
from datetime import date

# Find posts published after the start of 2024
posts_in_2024_onward = Post.objects.filter(published_date__gt=date(2024, 1, 1))

# Find posts that are either drafts or under review (if we had that status)
draft_or_review = Post.objects.filter(status__in=['draft', 'review'])

Crossing the Great Divide: Filtering on Relationships

This is where the Django ORM truly feels like magic. You can use the same double-underscore syntax to filter based on the fields of a related model (like a ForeignKey, ManyToManyField, or OneToOneField).

Our Post model has a ForeignKey to the built-in User model. Let's find all posts written by authors whose username starts with 'j'.

# Filter posts based on a field in the related User model
posts_by_j_authors = Post.objects.filter(author__username__istartswith='j')

Look at that! We simply followed the relationship: from Post to author, then from author to its username field. Django handles the complex SQL JOIN for you behind the scenes.

When `AND` Isn't Enough: Complex Queries with `Q` Objects

Chaining filters works great for AND conditions, but what if you need an OR? For example, how would you find posts where the title contains "Python" OR the content contains "Python"?

This is impossible with .filter() alone. For this, we need Q objects.

A Q object is an object used to encapsulate a portion of a SQL query. You can then combine these objects using the & (AND), | (OR), and ~ (NOT) operators.

First, import it:

from django.db.models import Q

Now, let's solve our "OR" problem:

# Find posts where 'Python' is in the title OR the content
python_posts = Post.objects.filter(
    Q(title__icontains='python') | Q(content__icontains='python')
)

You can create very complex logic by combining them. Let's find all published posts where the title contains "Django" OR the author's username is "admin".

# (title contains 'Django' OR author is 'admin') AND status is 'published'
complex_query = Post.objects.filter(
    Q(title__icontains='django') | Q(author__username='admin'),
    status='published'
)

Notice how we can mix regular filter arguments (status='published') with Q objects. The regular arguments are always AND-ed with the Q object expression.

A Quick Note on Performance

When filtering across relationships, be mindful of the "N+1 query problem". If you filter for a set of posts and then loop through them to access their authors, you'll make one extra database query for every single post.

# WARNING: This can be inefficient!
posts = Post.objects.filter(status='published') # 1 query
for post in posts:
    print(post.author.username) # 1 query PER post!

You can solve this by using select_related (for ForeignKey/OneToOne) or prefetch_related (for ManyToMany/reverse ForeignKey). These tell Django to fetch the related objects in a single, more efficient query.

# EFFICIENT:
posts = Post.objects.filter(status='published').select_related('author') # 1 query with a JOIN
for post in posts:
    print(post.author.username) # No extra query needed!

Conclusion: You're Ready to Filter

Mastering filtering is non-negotiable for building efficient and powerful Django applications. We've covered the core tools you'll use every day:

  • .filter() and .exclude() for basic queries.
  • Chaining to apply multiple AND conditions.
  • Field lookups (__) for precise matching on fields.
  • Spanning relationships to query across models.
  • Q objects for complex OR and NOT logic.

The best way to get comfortable is to open the Django shell (python manage.py shell) and start experimenting. Grab a QuerySet and see how you can slice and dice it. Happy querying!

You May Also Like