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.
Daniel Carter
A senior full-stack developer with over a decade of experience building with Django.
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:
# 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 complexOR
andNOT
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!