Django F Object

7 interview angles 6 min read source

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.field reads 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) and instance.field = value; instance.save()?”update() issues SQL directly; bypasses pre_save / post_save signals and auto_now fields. save() triggers signals, runs validation, and updates auto_now timestamps. F() works with both but bypasses signals via update().
  • “After instance.field = F('field') + 1; instance.save(), what’s instance.field?” — an F expression, not the updated integer. Must refresh_from_db() to read the new value. The cleaner pattern: skip the instance, use Model.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; no clean() or model validators run. Same as raw SQL update.