Q Objects & Chaining
Every .filter() call you wrote in the previous lesson narrowed a QuerySet down using
equality and lookups like __gt or __icontains. That covers a huge amount of
everyday querying. But there's a limitation baked into how .filter() works that you'll
hit the moment you need "either/or" logic instead of "all of the following must be true" logic. This
lesson is about recognizing that limitation and reaching for the right tool — Q objects
— when plain filtering isn't expressive enough.
Chained .filter() Calls Are Always AND
When you chain multiple .filter() calls, or pass multiple keyword arguments to a single
.filter() call, Django combines every condition with AND. There is no
way to make chained filters mean OR — it simply isn't what the method does. Here's a worked example
using TaskFlow's Task model:
# Both of these produce the exact same SQL: high-priority AND completed tasks
Task.objects.filter(priority='high', completed=True)
Task.objects.filter(priority='high').filter(completed=True)
Chaining a second .filter() never widens the result set — it only ever narrows it further,
because each call filters what the previous call already returned. If what you actually wanted was
"high priority or completed", neither version above gets you there. This is exactly the gap
Q objects fill.
The Q Object: Expressing OR Logic
A Q object, imported from django.db.models, wraps up a lookup as a standalone
value that can be combined with other Q objects using Python's | (OR),
& (AND), and ~ (NOT) operators before it's ever handed to
.filter(). A common real need in TaskFlow: show every task that needs attention right
now — either it's high priority, or it's already overdue.
from django.db.models import Q
from datetime import date
# Tasks that are EITHER high priority OR past their due date
needs_attention = Task.objects.filter(
Q(priority='high') | Q(due_date__lt=date.today())
)
Note that the | sits between two Q objects, not between two
.filter() calls — that's the whole trick. Each Q(...) holds exactly one
lookup, and the operator decides how they're combined once they're evaluated together.
Combining With AND, and Negating With NOT
Q objects support & for AND the same way | supports OR —
useful once conditions get nested deeper than a plain .filter() chain can express clearly.
~ negates a Q object, which is how you express "NOT this condition" for a
case the standard lookups don't have a direct opposite for.
# AND written explicitly with Q — equivalent to filter(priority='high', completed=False)
Task.objects.filter(Q(priority='high') & Q(completed=False))
# NOT — every task that is NOT assigned to the "Website Redesign" project
Task.objects.filter(~Q(project__name='Website Redesign'))
The & example is a bit redundant on its own — plain keyword arguments already do
AND for you — but it matters once you're mixing AND and OR together in the same expression, since
Python's normal operator precedence and parentheses then control exactly how the logic groups.
A Full Example: A Realistic Search Feature
A search box is the classic case for combining several Q objects with OR. Say TaskFlow
has a single search field, and typing a term should match it against a task's title
or the name of the project it belongs to:
from django.db.models import Q
def search_tasks(term):
return Task.objects.filter(
Q(title__icontains=term) | Q(project__name__icontains=term)
)
# A user searching "redesign" gets back tasks whose title mentions
# "redesign", PLUS tasks that belong to a project named e.g. "Website Redesign"
matches = search_tasks('redesign')
You can keep stacking Q objects and operators to build up arbitrarily complex conditions
— the key habit is thinking in terms of "which of these should be an OR, and which should be an AND"
before writing the expression.
F Expressions: Comparing Two Fields on the Same Row
Q objects combine conditions; F expressions solve a different problem —
referencing the value of another field on the same row inside a lookup, instead of a fixed
Python value. Normally .filter(some_field=5) compares a column to the constant
5. An F expression lets you compare a column to another column. If
TaskFlow's Task model tracked both an estimated_hours field and an
actual_hours field, finding every task that ran over estimate would look like this:
from django.db.models import F
# Tasks where actual_hours is greater than estimated_hours, on the SAME row
Task.objects.filter(actual_hours__gt=F('estimated_hours'))
This is a secondary tool compared to Q, but worth recognizing: whenever a filter needs to
reference "another field on this same record" rather than a fixed value you already know, F
is the mechanism.
Decision Guide: Plain Filters vs. Q Objects
| Situation | What to reach for |
|---|---|
| All conditions must be true together | Plain .filter(a=..., b=...) or chained .filter() calls — simplest and most readable |
| Any one of several conditions can be true | Q(a) | Q(b) |
| A condition should be excluded / inverted | ~Q(a) (or .exclude() for the simple single-condition case) |
| Mixing AND and OR in one expression | Q objects combined with & and |, grouped with parentheses |
| Comparing one field to another field on the same row | F expressions |
Task and Project models connect to each other, and how to query across that connection.