Skip to main content

Values & Annotate

Control the shape of your query results with values() and annotate().

values() โ€” Dict Resultsโ€‹

Instead of returning Model instances, return dictionaries:

posts = await Post.objects.values("title", "views")
# โ†’ [{"title": "Hello", "views": 100}, {"title": "World", "views": 200}]

# All fields as dicts
all_dicts = await Post.objects.values()

values_list() โ€” Tuple Resultsโ€‹

titles = await Post.objects.values_list("title", flat=True)
# โ†’ ["Hello", "World", "Foo"]

pairs = await Post.objects.values_list("title", "views")
# โ†’ [("Hello", 100), ("World", 200)]

annotate() โ€” Computed Fieldsโ€‹

Add calculated columns to each result row:

posts = await Post.objects.annotate(
comment_count=Count("comments.id"),
is_popular=Case(
When(views__gte=1000, then=True),
default=False,
),
)

print(posts[0]["comment_count"]) # Access annotated field

values() + annotate() = GROUP BYโ€‹

The most powerful pattern โ€” group by one or more fields:

# Posts per author with stats
await (
Post.objects
.values("author_id")
.annotate(posts=Count("id"), total_views=Sum("views"))
.order_by("-total_views")
)
# โ†’ [
# {"author_id": 1, "posts": 15, "total_views": 5000},
# {"author_id": 2, "posts": 8, "total_views": 3200},
# ]

Multiple Group By Fieldsโ€‹

# Posts per author per month
await (
Post.objects
.values("author_id", "status")
.annotate(count=Count("id"))
)
# โ†’ [
# {"author_id": 1, "status": "published", "count": 10},
# {"author_id": 1, "status": "draft", "count": 3},
# ]

Filtering Groups with exclude()โ€‹

# Authors with more than 5 posts
await (
Post.objects
.values("author_id")
.annotate(count=Count("id"))
.filter(count__gt=5)
)

This generates a HAVING clause:

SELECT "author_id", COUNT("id") AS "count"
FROM "posts"
GROUP BY "author_id"
HAVING COUNT("id") > 5

Next Stepsโ€‹

โ†’ Relationships โ€” Work with related models