A Q object wraps a single query condition in a reusable Python object so you can join conditions with OR (|), AND (&) and NOT (~). A plain .filter(**kwargs) call ANDs all of its arguments together, so it cannot express an OR like "title starts with Who or What" or a negation in one query. That is exactly the gap Q fills.
Import it from django.db.models and you can use it in filter(), exclude(), get(), and inside conditional expressions like When/Case:
from django.db.models import QThe examples below use this small Django model. Every Q lookup uses the same field__lookup=value syntax you already know from .filter().
from django.db import models
class Author(models.Model):
name = models.CharField(max_length=120)
country = models.CharField(max_length=2) # ISO code, e.g. "GB"
class Post(models.Model):
title = models.CharField(max_length=200)
body = models.TextField()
status = models.CharField(max_length=20) # "draft" | "published"
is_featured = models.BooleanField(default=False)
views = models.PositiveIntegerField(default=0)
author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name="posts")
published_at = models.DateTimeField(null=True, blank=True)Basic Q object usage
A single Q is equivalent to passing the same keyword to .filter() — useful on its own only as a building block. The two queries below are identical:
from django.db.models import Q
# These produce the same SQL
Post.objects.filter(Q(title__startswith="What"))
Post.objects.filter(title__startswith="What")Combining Q objects: OR (|), AND (&), NOT (~)
This is where Q earns its place. Use | for OR, & for AND, and ~ to negate a condition. Always wrap groups in parentheses so the precedence is explicit.
# OR: title starts with "Who" OR "What"
Post.objects.filter(Q(title__startswith="Who") | Q(title__startswith="What"))
# AND: published AND featured (explicit & — same as passing both kwargs)
Post.objects.filter(Q(status="published") & Q(is_featured=True))
# NOT: everything that is NOT a draft
Post.objects.filter(~Q(status="draft"))
# Mixed: (published OR featured) AND author not in India
Post.objects.filter(
(Q(status="published") | Q(is_featured=True)) & ~Q(author__country="IN")
)If you do not include an operator between two Q objects passed as separate arguments, Django joins them with AND by default.
Mixing Q objects with keyword arguments
You can pass Q objects and normal keyword arguments to the same call, but the positional Q arguments must come first — otherwise Python raises a SyntaxError. The comma between them acts as AND.
# Correct: Q args first, keyword args after — joined with AND
Post.objects.filter(
Q(title__startswith="Who") | Q(title__startswith="What"),
status="published",
)
# Wrong: positional Q after a keyword argument -> SyntaxError
# Post.objects.filter(status="published", Q(title__startswith="Who"))Building dynamic queries programmatically
Because Q objects are plain Python objects, you can build them in a loop and fold them together with functools.reduce and the operator module. This is the idiomatic way to turn a variable-length list of conditions into one query.
import operator
from functools import reduce # reduce is not a builtin in Python 3
from django.db.models import Q
def search_titles(prefixes):
"""OR together a dynamic list of title prefixes."""
if not prefixes:
return Post.objects.none() # guard: empty list would match everything
query = reduce(operator.or_, (Q(title__startswith=p) for p in prefixes))
return Post.objects.filter(query)
# AND instead of OR — swap the operator
def match_all(filters):
query = reduce(operator.and_, (Q(**f) for f in filters), Q())
return Post.objects.filter(query)A bare Q() matches everything (an empty WHERE), which is the correct identity for AND but a trap for OR — an empty input list would return all rows instead of none. That is why search_titles returns .none() for an empty list.
Q objects across relations and spanning lookups
Q supports the same double-underscore relationship traversal as .filter(), so you can branch across foreign keys in a single OR.
# Authors from the UK, OR anyone whose name contains "smith"
Post.objects.filter(
Q(author__country="GB") | Q(author__name__icontains="smith")
)
# __in works inside a Q just like anywhere else
Post.objects.filter(Q(status="published") & Q(author_id__in=[1, 2, 3]))Combining Q with .exclude()
.exclude() negates the whole condition you give it. Wrapping an OR inside a single exclude() is not the same as chaining .exclude().exclude(), so reach for Q when you need precise negation.
# NOT (draft OR author in India) == published-ish AND author not in India
Post.objects.exclude(Q(status="draft") | Q(author__country="IN"))
# Equivalent using ~Q inside filter
Post.objects.filter(~(Q(status="draft") | Q(author__country="IN")))A practical multi-field search box
The most common real-world use of Q is a search input that should match across several columns. Build one OR query over every searchable field, then add select_related to avoid an N+1 query and distinct() because spanning a multi-valued relation can duplicate rows.
import operator
from functools import reduce
from django.db.models import Q
def post_search(term):
term = (term or "").strip()
if not term:
return Post.objects.all()
fields = ["title", "body", "author__name"]
query = reduce(operator.or_, (Q(**{f"{f}__icontains": term}) for f in fields))
return (
Post.objects.filter(query)
.select_related("author") # avoid N+1 when reading post.author
.distinct() # de-dup rows from the relation join
)When to use which
| Approach | Best for | Logical join | Example |
|---|---|---|---|
.filter(a=1, b=2) |
Simple AND conditions | AND only | Post.objects.filter(status="published", is_featured=True) |
Chained .filter().filter() |
AND across multi-valued relations (each clause applies separately) | AND | qs.filter(tags__name="x").filter(tags__name="y") |
Q with | / & / ~ |
OR, NOT, or mixed boolean logic | Any combination | filter(Q(a=1) | Q(b=2)) |
Dynamic Q + reduce |
Variable-length / user-supplied conditions | Any (pick the operator) | filter(reduce(operator.or_, q_list)) |
.exclude(...) |
Negating an entire condition | NOT (whole clause) | exclude(Q(status="draft") | Q(views=0)) |
Q objects in conditional expressions
A Q can also be the condition inside When/Case, so you can annotate computed values based on complex boolean logic. See our deeper write-up on conditional expressions in queries.
from django.db.models import Case, When, Value, BooleanField, Q
Post.objects.annotate(
is_hot=Case(
When(Q(views__gte=1000) | Q(is_featured=True), then=Value(True)),
default=Value(False),
output_field=BooleanField(),
)
)Common gotchas
- Operator precedence. Python evaluates
&before|, and~binds tightly — but do not rely on memory. Always parenthesize groups so the SQL matches your intent. - Q must precede keyword arguments in the same call, or you get a
SyntaxError. - Empty
Q()matches everything. Guard dynamic lists (return.none()for empty OR input) so a missing filter does not leak the whole table. Qonly builds theWHEREclause. It does not fetch related rows — pair it withselect_related/prefetch_relatedand read our database access optimization guide to keep queries fast.- Spanning multi-valued relations can duplicate rows; add
.distinct()when an OR crosses a one-to-many or many-to-many join.
Frequently Asked Questions
What is a Q object in Django?
A Q object (from django.db.models import Q) encapsulates a single SQL query condition as a Python object. On its own it behaves like a .filter() keyword, but because it is an object you can combine several with boolean operators to express logic that plain keyword filtering cannot.
When should I use Q objects instead of .filter() keywords?
Use plain .filter(a=1, b=2) when every condition is ANDed together — it is shorter and clearer. Reach for Q the moment you need an OR, a NOT, mixed boolean logic, or a query built dynamically from a variable list of conditions.
How do I write an OR query in Django?
Combine two Q objects with the | operator: Post.objects.filter(Q(title__startswith="Who") | Q(title__startswith="What")). Each Q holds one condition and | joins them into a SQL OR. Wrap groups in parentheses when mixing with &.
Can I mix Q objects with keyword arguments in filter()?
Yes, but the positional Q objects must come before any keyword arguments — filter(Q(a=1) | Q(b=2), status="published"). Putting a Q after a keyword argument raises a SyntaxError. The comma between them is treated as AND.
How do I build a dynamic query from a list of conditions?
Create a list of Q objects and fold them with functools.reduce plus the operator module: reduce(operator.or_, q_list) for OR or reduce(operator.and_, q_list) for AND. Guard against an empty list, since an empty Q() matches every row.
Do Q objects work with exclude() and get()?
Yes. Q works in filter(), exclude(), and get(), and as the condition inside When/Case expressions. Note that exclude(Q(a) | Q(b)) negates the whole OR — equivalent to filter(~(Q(a) | Q(b))).
At MicroPyramid we have built Django applications and REST APIs for startups and enterprises for 12+ years across 50+ projects, and clean, dynamic ORM queries with Q objects are part of how we keep those data layers fast and maintainable.