Django F Object
F() references the value of a model field at the SQL layer, without loading the object into Python. Used for atomic updates, field-to-field comparisons, and expressions in queries. Solves race conditions that hand-rolled “load → modify → save” Python code can’t.
For Q objects (logical conditions) see 02_q_object.md. For the select_related/prefetch_related side of ORM optimization see 11_select_related_vs_prefetch_related.md.
The race condition F() solves
# BAD — race condition
post = Post.objects.get(id=42)
post.view_count = post.view_count + 1
post.save()
What happens with two concurrent requests:
Time Process A Process B
───── ─────────────── ───────────────
0 get(id=42) → view_count = 100
1 get(id=42) → view_count = 100
2 view_count = 101
3 view_count = 101
4 save() → DB now has 101
5 save() → DB now has 101 (should be 102)
Two increments, one increment recorded. The classic lost-update bug.
Fix with F():
# GOOD — atomic at the DB level
Post.objects.filter(id=42).update(view_count=F("view_count") + 1)
Generates:
UPDATE post SET view_count = view_count + 1 WHERE id = 42;
The DB does the arithmetic. Both concurrent requests succeed; final count is correct.
Where F() shines
Atomic counters / accumulators
# Increment view count
Post.objects.filter(id=post_id).update(view_count=F("view_count") + 1)
# Decrement stock when an order is placed
Product.objects.filter(id=prod_id, stock__gte=qty).update(stock=F("stock") - qty)
The second example is a “conditional decrement” — the stock__gte=qty filter ensures we only decrement if there’s enough. If two concurrent orders try to buy the last unit, exactly one succeeds.
Field-to-field comparison
# Employees whose salary exceeds their bonus
Employee.objects.filter(salary__gt=F("bonus"))
# Orders where shipped date is before order date (data integrity check)
Order.objects.filter(shipped_at__lt=F("created_at"))
You can’t do salary__gt="bonus" because Django would treat that as a string literal. F("bonus") tells the ORM “the column, not a literal.”
Annotations with computed values
from django.db.models import F, Sum
Order.objects.annotate(line_total=F("price") * F("quantity"))
Each row gets a computed line_total. Available for further filtering / ordering:
expensive_orders = Order.objects.annotate(
total=F("price") * F("quantity")
).filter(total__gt=1000)
The computation happens in SQL: SELECT price * quantity AS line_total .... No Python loop.
Update relative to current value
# Bulk price increase
Product.objects.filter(category="books").update(
price=F("price") * 1.1
)
Single SQL statement: UPDATE product SET price = price * 1.1 WHERE category = 'books'. Atomic, fast, no row-by-row Python.
F() with save() — refresh the instance after
post = Post.objects.get(id=42)
post.view_count = F("view_count") + 1
post.save()
# post.view_count is now an F expression, NOT an integer
print(post.view_count) # <CombinedExpression: F(view_count) + Value(1)>
# Refresh to get the actual value:
post.refresh_from_db()
print(post.view_count) # 101 (or whatever)
After save() with F(), the instance’s field holds the expression, not the new value. Accessing it for further calculations doesn’t work — you must refresh_from_db().
The cleaner pattern is to use .update() directly without an instance:
Post.objects.filter(id=42).update(view_count=F("view_count") + 1)
# No instance state to worry about
Use the instance + save() pattern only when you need post-save signals to fire (update() bypasses them).
F() in conditional expressions
from django.db.models import Case, When, F, Value
User.objects.annotate(
discount=Case(
When(loyalty_years__gte=5, then=F("base_discount") * 2),
default=F("base_discount"),
)
)
Case/When + F() lets you build conditional updates at the SQL layer.
Common pitfalls
Accessing the field after F() update without refresh
post.view_count = F("view_count") + 1
post.save()
if post.view_count > 100: # post.view_count is an F expression, not int
notify()
Will fail or produce surprising results. refresh_from_db() first, or just use the value before the update.
Forgetting that F() is per-field
# Won't work as intended
Post.objects.filter(id=42).update(
view_count=F("view_count") + 1,
last_viewed=now(),
update_count=F("update_count") + 1,
)
This works fine — F() applies to specific fields. The pitfall would be expecting F() to refer to “current row” as a whole or mixing field references in ways the ORM can’t translate.
Using F() in .filter() with a non-F value
# Confusing if you don't know the rule
Order.objects.filter(quantity=F("max_per_user")) # OK — compares two columns
Order.objects.filter(quantity=5) # OK — compares column to literal
Order.objects.filter(quantity="max_per_user") # treats "max_per_user" as literal string
If you want a column reference, you must use F(). Django won’t infer it.
Multi-table joins with F()
Comment.objects.filter(created_at__lt=F("post__published_at"))
This works for following ForeignKey chains. Generates a JOIN.
F() and update() bypass signals + auto_now
class Post(models.Model):
view_count = models.IntegerField(default=0)
updated_at = models.DateTimeField(auto_now=True)
Post.objects.filter(id=42).update(view_count=F("view_count") + 1)
# updated_at is NOT updated. auto_now only fires on save(), not update().
If you need auto_now to fire, either include it explicitly in the update or use .save():
Post.objects.filter(id=42).update(
view_count=F("view_count") + 1,
updated_at=now(),
)
Same with post_save signals — update() bypasses them. Tradeoff: faster, atomic; but the side effects of save() don’t run.
F() in INSERT (not supported in the obvious way)
# Doesn't work for new objects:
Post(view_count=F("default_count") + 1).save() # no
F() requires an existing row to reference. For INSERT, compute the value in Python or use a database-level default.
F() with database functions
Combine with django.db.models.functions:
from django.db.models import F
from django.db.models.functions import Coalesce, Greatest
Product.objects.annotate(
final_price=Coalesce(F("sale_price"), F("base_price")),
)
Employee.objects.update(
salary=Greatest(F("salary") * 1.1, Value(50000)),
)
Build complex SQL expressions without writing raw SQL.
F() vs raw SQL
# F() — Django ORM
Post.objects.filter(id=42).update(view_count=F("view_count") + 1)
# Equivalent raw SQL via Django's connection
from django.db import connection
with connection.cursor() as cursor:
cursor.execute("UPDATE post SET view_count = view_count + 1 WHERE id = %s", [42])
Both produce the same SQL. F() is preferred:
- Type-safe (the model knows the column).
- Refactorable (rename the field, the F() reference moves with it).
- Composable with other ORM features (Case, When, annotations).
Drop to raw SQL only when ORM can’t express what you need.
Concurrency model — F() vs SELECT FOR UPDATE
| Approach | When to use |
|---|---|
| F() in update() | atomic single-field-update, no conditional logic required beyond filter |
select_for_update() + save() |
multi-field updates that depend on read values, business logic in Python |
Optimistic locking (version_id_col) |
rare conflicts, retry on conflict |
# select_for_update + Python logic
with transaction.atomic():
account = Account.objects.select_for_update().get(id=42)
if account.balance >= amount:
account.balance -= amount
account.save()
# ... other side effects ...
This holds a row lock for the entire transaction. Other transactions wait. Slower than F() but allows arbitrary Python logic in the middle.
Use F() for simple “increment/decrement” cases. Use select_for_update() for cases needing Python branches or multi-step state changes. See ../../08_databases/sql/08_transactions_isolation.md.
Common interview confusions
- “F() is for fetching data.” — no, it’s for referencing a field at the SQL layer. Used inside
.filter(),.update(),.annotate(),.aggregate(). - “F() solves all concurrency issues.” — solves single-field atomic-update cases. Multi-field consistency still needs
select_for_update()or transactions. - “F() works the same as model_instance.field.” —
instance.fieldreads the current Python value;F("field")references the column in SQL.
Interview angle
- “What’s the F object in Django and when do you use it?” — references a model field at the SQL layer. Used for atomic updates (
update(count=F("count") + 1)), field-to-field comparisons (filter(salary__gt=F("bonus"))), and computed values in annotations / aggregates. Replaces unsafe “load-modify-save” patterns that have race conditions. - “Walk through a race condition F() prevents.” — two processes both read view_count=100, both compute 101 in Python, both save 101. Final value is 101 instead of 102. With
update(view_count=F("view_count") + 1), the DB does the arithmetic atomically; final value is 102. - “What’s the difference between
.update(field=value)andinstance.field = value; instance.save()?” —update()issues SQL directly; bypasses pre_save / post_save signals andauto_nowfields.save()triggers signals, runs validation, and updatesauto_nowtimestamps. F() works with both but bypasses signals viaupdate(). - “After
instance.field = F('field') + 1; instance.save(), what’sinstance.field?” — an F expression, not the updated integer. Mustrefresh_from_db()to read the new value. The cleaner pattern: skip the instance, useModel.objects.filter(...).update(field=F('field') + 1). - “F() vs select_for_update?” — F() for atomic single-field math with no conditional logic;
select_for_update()for cases that need to read values, branch in Python, then write multiple fields under a row lock. F() is faster (one statement, no lock held). - “Can you use F() in INSERT statements?” — no. F() references existing column values; INSERT has nothing to reference. Compute Python-side, or use DB-level defaults.
- “Does F() bypass model validation?” — yes, when used with
.update(). The ORM doesn’t fetch the row; noclean()or model validators run. Same as raw SQL update.