2025, Nov 04 19:00
How to fix SyntaxError: positional argument follows keyword in Django annotate with filtered Count and Q
Learn why Django ORM annotate with filtered Count can throw 'positional argument follows keyword' and how to fix it by reordering arguments, using Q filters.
Counting related rows with a condition should be straightforward in Django ORM, yet a seemingly harmless refactor can explode with a syntax error long before the database ever sees the query. Here’s a practical walkthrough of a common pitfall when using annotate with filtered Count and how to fix it without second-guessing your Q logic.
Problem statement
The goal is to aggregate the number of related EventReport objects per Store while excluding records where status equals "C". The refactor replaces per-row queries with annotate and a filtered Count to avoid the N+1 problem.
However, the call ends up raising SyntaxError: positional argument follows keyword argument. That quickly distracts from the actual aggregation and leads to doubts about whether Q(...) & ~Q(...) is even valid inside Count(..., filter=...).
Failing example
The intention is to express a date range filter and exclude status="C" in a single aggregation:
monitored_stores_qs.annotate(
reports_total=Count(
'eventreport',
filter=Q(
eventreport__event_at__gte=window_start,
eventreport__event_at__lte=yesterday_cutoff
) & ~Q(eventreport__status='C')
),
Count('another_metric'),
)
There is also a variant that builds the filter expression the same way:
filter=Q(
eventreport__event_at__gte=range_start,
eventreport__event_at__lte=range_end
) & ~Q(eventreport__status='C')
The expectation is to get only related EventReport rows that fall within the date window and are not marked with status "C".
What actually goes wrong
This is not a Django error; Python raises it. The core issue is the ordering of arguments in a function call. In Python, positional arguments must appear before keyword arguments. A minimal example looks like this: f(x, y=z) is valid, while f(y=z, x) is not. In the aggregation code, the annotate call mixes a keyword argument followed by a bare positional argument, which triggers the syntax error.
The Q(...) & ~Q(...) expression itself is fine for building the filter; the parser never reaches Django’s logic because Python stops at the invalid call signature.
Fix and corrected example
Keep the filtered Count as is and ensure that any positional arguments come before keyword arguments in the same call. Reordering the annotate call resolves the problem:
monitored_stores_qs.annotate(
Count('another_metric'),
reports_total=Count(
'eventreport',
filter=Q(
eventreport__event_at__gte=window_start,
eventreport__event_at__lte=yesterday_cutoff
) & ~Q(eventreport__status='C')
),
)
This preserves the intent: a single pass over related EventReport rows, counted per Store, within the desired date interval and excluding status "C". The critical change is purely about argument ordering, not about the aggregation logic or Q expressions.
Why this matters
When optimizing ORM code to eliminate N+1 queries, it’s easy to attribute errors to the database layer or Django’s query expressions. In reality, a Python-level syntax rule can derail the whole operation before any query is constructed. Reading the full error message and checking the call signature saves time and avoids misdiagnosing the Q and ~Q pattern, which in this case is not the culprit.
Conclusion
If you see SyntaxError: positional argument follows keyword argument while building annotate calls, look first for positional arguments that come after named ones in the same function call. For filtered aggregates, the pattern Count('eventreport', filter=Q(...) & ~Q(...)) accurately expresses status != 'C' alongside other conditions. Keep the logic, fix the argument order, and the aggregation will work as intended.
The article is based on a question from StackOverflow by Raul Chiarella and an answer by willeM_ Van Onsem.