Hello! Welcome back.
In our last lesson, you got your hands dirty with the fundamental Create, Retrieve, Update, and Delete (CRUD) operations using the Django ORM. Working in the Django shell, you learned how to use methods like .create(), .get(), .all(), .update(), and .delete() to manage your data. That's a huge step!
Now, it's time to go from simply retrieving data to interrogating it. Real-world applications require you to find very specific pieces of information: tasks due this week, incomplete tasks for a specific user, or the most recently created items.
This lesson builds directly on what you've learned, focusing on how to query your database with precision. You will master using filter(), exclude(), and order_by() with advanced techniques to retrieve exactly the data you need.

We'll continue our work in the Django shell, as it's the perfect environment to experiment with and master these powerful querying capabilities.
A Quick Setup
To make our queries interesting, we need some varied data to work with. Open your Django shell and let's create a few users and tasks. For the sake of these examples, imagine we've added a due_date field to our Task model (due_date = models.DateField(null=True, blank=True)).
Run the following code in your shell to set up our practice data:
# In the Django shell
from tasks.models import Task
from django.contrib.auth.models import User
from datetime import date, timedelta
# Get or create users
user_alice, _ = User.objects.get_or_create(username='alice')
user_bob, _ = User.objects.get_or_create(username='bob')
# Clear out any old tasks to start fresh
Task.objects.all().delete()
# Create a sample set of tasks
Task.objects.create(title='Finalize report', description='Draft and send the Q3 report', due_date=date.today(), completed=False, user=user_alice)
Task.objects.create(title='Plan team meeting', description='Set agenda for the weekly sync', due_date=date.today() + timedelta(days=2), completed=False, user=user_alice)
Task.objects.create(title='Buy groceries', description='Milk, bread, and eggs', due_date=date.today() + timedelta(days=1), completed=True, user=user_bob)
Task.objects.create(title='Fix login bug', description='Users reporting 500 error on login page', due_date=date.today() - timedelta(days=1), completed=False, user=user_bob)
Task.objects.create(title='Follow up on client feedback', description='', due_date=None, completed=False, user=user_alice)
print("Sample data created!")
With our data in place, let's start querying.
1. Advanced Filtering with Field Lookups
You're already familiar with filter() for exact matches (e.g., Task.objects.filter(completed=True)). But what if you want to find tasks due after a certain date, or tasks whose titles contain a specific word? For this, we use field lookups.
Field lookups are specified using a double underscore (__) after the field name. For example, price__gt=100 translates to "where the price is greater than 100".
The following video is an excellent deep dive into filtering, lookups, and ordering. We'll be watching it in sections.
Django ORM - QuerySet Filtering and Lookups / Ordering and Slicing QuerySets
Watch this comprehensive video from BugBytes, "Django ORM - QuerySet Filtering and Lookups / Ordering and Slicing QuerySets", to see these concepts in action. We'll start with filtering and lookups.
Watch the following segments to get a strong grasp of filtering and lookups: filter() and exclude() (04:34 - 06:41 and 15:14 - 17:56): This is a great refresher on filter() and introduces its inverse, exclude(), which finds records that do not match the criteria. The in lookup (14:15 - 15:24): Learn how to filter for objects where a field's value is within a list of possibilities. Comparison and Range Lookups (17:46 - 22:48): This is the core of advanced filtering. Pay close attention to lookups like __gt (greater than), __lt (less than), and __range.
Here is a quick reference table of some of the most common lookups:
| Lookup | Meaning | Example |
|---|---|---|
exact |
Exact match (the default) | Task.objects.filter(title__exact='Finalize report') |
iexact |
Case-insensitive exact match | Task.objects.filter(title__iexact='finalize report') |
contains |
Contains substring (case-sensitive) | Task.objects.filter(title__contains='report') |
icontains |
Case-insensitive contains | Task.objects.filter(description__icontains='Login') |
gt, gte |
Greater than, Greater than or equal to | Task.objects.filter(due_date__gt=date.today()) |
lt, lte |
Less than, Less than or equal to | Task.objects.filter(due_date__lte=date.today()) |
in |
In a list of values | Task.objects.filter(id__in=[1, 3, 5]) |
startswith |
Starts with (case-sensitive) | Task.objects.filter(title__startswith='Plan') |
range |
Between two values (inclusive) | Task.objects.filter(due_date__range=[start_date, end_date]) |
isnull |
Is NULL or is not NULL |
Task.objects.filter(due_date__isnull=True) |
Test your understanding!
Using the sample data you created, write a single Django ORM query to find all tasks that are not yet completed and are overdue (i.e., their due date is before today).
Show answer
You can chain multiple filter() conditions together. Django translates these into AND in the final SQL query.
# In the Django shell
from datetime import date
overdue_tasks = Task.objects.filter(completed=False, due_date__lt=date.today())
print(overdue_tasks)
# You should see the "Fix login bug" task.
2. Querying Across Relationships
One of the most powerful features of the ORM is the ability to filter based on fields in related models. You do this using the same double-underscore (__) syntax to "span" the relationship.
For example, to find all tasks assigned to the user named alice:
# The first 'user' is the ForeignKey field on the Task model.
# The second 'username' is the field on the related User model.
>>> Task.objects.filter(user__username='alice')
<QuerySet [<Task: Finalize report>, <Task: Plan team meeting>, <Task: Follow up on client feedback>]>
This is incredibly useful and allows you to build complex queries without needing to fetch intermediate objects.
Django ORM - QuerySet Filtering and Lookups / Ordering and Slicing QuerySets
Let's return to the BugBytes video to see how to filter across foreign keys.
Watch the section on Filtering by Foreign Key (35:48 - 38:47). The video clearly explains how Django uses a JOIN in the underlying SQL to make this work.
3. Sorting Results with order_by()
By default, query results are not guaranteed to be in any specific order. To control the sorting, you use the order_by() method.
- Ascending Order:
Task.objects.order_by('due_date') - Descending Order: Prepend a hyphen (
-) to the field name.Task.objects.order_by('-due_date')will put the tasks with the latest due dates first. - Multiple Fields: You can sort by multiple criteria.
Task.objects.order_by('completed', '-due_date')will sort by completion status first (False then True), and then by due date (newest first) within each group.
Django ORM - QuerySet Filtering and Lookups / Ordering and Slicing QuerySets
The BugBytes video also has an excellent segment on ordering.
Watch the section on Ordering Records (22:48 - 29:00). It covers ascending/descending order and even touches on advanced topics like case-insensitive ordering.
Test your understanding!
Write a query to retrieve all tasks for the user alice, ordered so that incomplete tasks appear before completed ones, and within those groups, the tasks are sorted by their title alphabetically.
Show answer
You need to filter by the user and then apply a multi-field order_by().
# In the Django shell
alice_tasks = Task.objects.filter(user__username='alice').order_by('completed', 'title')
print(alice_tasks)
This query first filters for Alice's tasks, then sorts them by the completed field (False comes before True), and finally sorts alphabetically by title for tasks with the same completion status.
A Note on get() vs. filter()
As a final reminder from the last lesson, remember the crucial difference between get() and filter():
filter()returns a QuerySet (a list-like object). It can contain zero, one, or many items. It never raises an error if no items are found.get()returns a single model instance. It is strict:- If it finds zero records, it raises a
DoesNotExisterror. - If it finds more than one record, it raises a
MultipleObjectsReturnederror.
- If it finds zero records, it raises a
Use get() only when you are certain your query will match exactly one object, typically when querying by a primary key (id) or another unique field.
Django ORM - QuerySet Filtering and Lookups / Ordering and Slicing QuerySets
Watch this short segment from the video to reinforce the behavior of get() and the errors it can produce.
Watch the clip Explaining the get() method (07:36 - 09:53). Understanding when get() will fail is key to writing robust code.
Extra Practice
Your preference is for practice-heavy learning, which is a great way to master the ORM. The following article provides a large set of practice problems with solutions. The models are for a library (Author, Book, Publisher), not our task manager, but the principles are identical. This is a good test of your ability to apply these concepts to a new data structure.
Django ORM — Examples and Practice Problems
For more practice, check out the article "Django ORM — Examples and Practice Problems". It contains a great set of exercises to test your skills.
First, review the models in the django_orm_dump_final1.sql code block at the start of the article to understand the data structure. Then, try solving a few problems from the Exercises list. For example: Problem 4: Combines a startswith lookup with a gte lookup. Problem 11: Uses a year__gte date-based lookup. Problem 28: A perfect use case for exclude(). After you attempt them, check your answers against the Solutions section at the end.
Conclusion
You've now added some serious firepower to your data-querying toolkit. By combining filter(), exclude(), and order_by() with field lookups and relationship spanning, you can retrieve very specific slices of data from your database efficiently and expressively.
Key Takeaways:
- Field Lookups (
__gt,__icontains, etc.) are used withfilter()andexclude()to create specific conditions. - Chaining methods like
filter(...).filter(...).order_by(...)allows you to build complex queries step-by-step. - Querying across relationships with the double-underscore syntax (
user__username) is a powerful way to filter based on related models. order_by()gives you full control over the sorting of your results, in both ascending and descending order.- Always use
get()for a single, unique object andfilter()for everything else.
Preview of the Next Lesson:
So far, all our work has been in the Django shell. It's a fantastic tool for learning and debugging, but it's not a web application. In the next module, we will finally leave the shell behind and start building the visible parts of our project. You will learn how to set up a Django project and app, create views to handle web requests, and map URLs to those views. This is where your backend logic will start to take shape and interact with the outside world.