Django

Q objects and F expressions — OR queries and race-condition-free updates

Plain keyword filters can only ever mean AND. Q objects add OR/NOT to queryset filtering, and F expressions let a query reference another field's CURRENT database value instead of a value already loaded into Python — the difference between an atomic update and a race condition.

Intermediate

3 min read

The limit of plain keyword filtering: it can only mean AND

Article.objects.filter(status="published", author=user)
# WHERE status = 'published' AND author_id = user.id — AND is the ONLY option here

Chaining .filter(a=1, b=2), or chaining multiple .filter() calls, always combines conditions with AND — there's no keyword-argument syntax for "status is published OR author is this specific user, whichever." That's precisely the gap Q objects exist to fill.

Q objects: OR, AND, and NOT, combined with real operators

from django.db.models import Q
 
# published OR owned by this user
Article.objects.filter(Q(status="published") | Q(author=user))
 
# published AND NOT flagged
Article.objects.filter(Q(status="published") & ~Q(is_flagged=True))
 
# (published OR featured) AND author is this user
Article.objects.filter((Q(status="published") | Q(featured=True)) & Q(author=user))

A Q object wraps a single condition into an object that supports | (OR), & (AND — the same as plain keyword chaining, but explicit), and ~ (NOT), which can be combined and nested with parentheses exactly like a boolean expression in plain Python. This is the only way to express "OR" in a Django queryset filter — there's no other syntax for it.

F expressions: referencing a field's value inside the database, not in Python

from django.db.models import F
 
# WRONG — a classic race condition
article = Article.objects.get(id=1)
article.views = article.views + 1     # reads views into Python, e.g. 41
article.save()                          # writes 42 — but another request may have
                                          # also read 41 and ALSO written 42, losing a view
 
# RIGHT — F() builds the +1 into the SQL itself
Article.objects.filter(id=1).update(views=F("views") + 1)
# UPDATE article SET views = views + 1 WHERE id = 1 — the database does the increment atomically

article.views + 1 in Python reads the current value into memory, computes the new value in Python, and writes it back — if two requests do this concurrently, both can read the same starting value and both write the same "incremented" result, silently losing one of the increments. F("views") instead tells Django to build the increment as part of the SQL statement itself (SET views = views + 1), which the database executes as a single atomic operation — there's no window where a concurrent request can read a stale value, because the increment never passes through Python at all.

F expressions also compare fields to each other, not just a field to a constant

# products where the discount price is somehow higher than the regular price — a data bug
Product.objects.filter(discount_price__gt=F("price"))
 
# orders that took longer than their estimated delivery time
Order.objects.filter(actual_delivery__gt=F("estimated_delivery"))

Without F, a filter's right-hand side is always a Python value passed in from outside the query. F("price") lets the right-hand side be another column on the same row, evaluated by the database during the query — something that has no equivalent using plain keyword filters at all, since there's no way to pass "the value of another field" as an ordinary Python argument.

Combining Q and F: filtering by a cross-field OR condition

Order.objects.filter(
    Q(status="delayed") | Q(actual_delivery__gt=F("estimated_delivery"))
)

Q and F solve different problems and compose cleanly together — Q controls how conditions combine (AND/OR/NOT), F controls what a condition is compared against (another field, computed in the database, instead of a fixed value). Real reporting and admin-tooling queries often need both at once, exactly as shown here.

Further reading

Check your understanding

A quick comprehension check — not tracked, not graded, just for you.

1. What logical operator does chaining `.filter(a=1, b=2)` always express?

2. What does `Q(status="published") | Q(author=user)` express?

3. Why does `article.views += 1; article.save()` risk losing an increment under concurrency, while `Article.objects.filter(id=1).update(views=F("views") + 1)` doesn't?

4. What can F() express that a plain keyword filter value cannot?