2025, Nov 06 19:00

How to Count Mutual Followers in Django: Keep Logic in Querysets, Use Annotate, Exists, and Prefetch for Speed

Learn why counting mutual followers in Django templates fails and how to fix it. Compute mutuals in views with annotate, Exists and Prefetch for fast rendering.

Counting mutual followers directly in a Django template looks tempting: you already have the images, so why not call count or use length right where you render them? The catch is that Django templates are intentionally limited and not meant for data processing. This guide shows what goes wrong, why it goes wrong, and how to compute mutuals and their count efficiently in the view layer where this logic belongs.

Problem statement

You have a self-referential follow graph and can display mutual followers’ avatars. But attempting to get the count in the template with count or length fails or returns the wrong number. The root cause is mixing data selection with presentation.

Minimal reproduction of the issue

# models.py
from django.conf import settings
from django.db import models
class Persona(models.Model):
    account = models.OneToOneField(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        blank=True,
        null=True,
    )
    avatar = models.ImageField(
        upload_to='UploadedProfilePicture/',
        default='ProfileAvatar/avatar.png',
        blank=True,
    )
    follows = models.ManyToManyField(
        'Persona',
        symmetrical=False,
        related_name='backers',
        blank=True,
    )
# views.py
from django.db.models import Q
from django.shortcuts import render
def network_view(request):
    page_title = 'Following'
    everyone = Persona.objects.exclude(Q(account=request.user))
    my_follow_ids = request.user.persona.follows.values_list('pk', flat=True)
    mutual_pool = Persona.objects.filter(pk__in=my_follow_ids)
    return render(
        request,
        'my_template.html',
        {
            'people': everyone,
            'mutual_pool': mutual_pool,
        },
    )
{% comment %} my_template.html {% endcomment %}
{% for person in people %}
  <p>{{ person.account }}</p>
  {% for candidate in mutual_pool %}
    {% if candidate in person.follows.all %}
      <img src="{{ candidate.avatar.url }}" class="hover-followers-img" />
      {{ candidate.count }}
    {% endif %}
  {% endfor %}
{% endfor %}

Two things go wrong here. First, candidate is a single object, not a collection, so calling count on it makes no sense. Second, the logic to select only mutuals is embedded in the template with an in check and repeated per row, which is inefficient and makes counting awkward.

What actually goes wrong

Django’s template language is deliberately restricted. It prevents calling arbitrary callables, passing parameters into methods, or writing complex data-selection logic. Even when you can work around this with filters, doing selection in the template is both hard to maintain and inefficient. Selection, aggregation, and prefetching belong in the view or queryset; templates should only render data.

The right approach: compute mutuals and their count in the view

Move all filtering and aggregation into the queryset. Use annotate for counts and prefetch_related with a filtered queryset for the mutual follower objects you want to display. Then the template can simply render avatars and print the already-computed number.

Solution for current Django (with Exists and Prefetch)

# views.py
from django.db.models import Count, Exists, OuterRef, Prefetch, Q
from django.db.models.functions import Greatest
from django.shortcuts import render
# Note: snake_case is canonical for function names in Python
def network_view(request):
    page_title = 'Following'
    people = (
        Persona.objects
        .exclude(Q(account=request.user))
        .annotate(
            # Prevent negatives by clamping to zero
            mutual_total=Greatest(
                Count(
                    'follows',
                    filter=Q(follows__backers__account=request.user)
                ) - 2,
                0,
            )
        )
        .prefetch_related(
            Prefetch(
                'follows',
                Persona.objects.filter(
                    Exists(
                        Persona.follows.through.objects.filter(
                            from_persona_id=OuterRef('pk'),
                            to_persona__account=request.user,
                        )
                    )
                ),
                to_attr='mutual_peers',
            )
        )
    )
    return render(
        request,
        'my_template.html',
        {
            'people': people,
        },
    )

Template rendering becomes straightforward and fast because every row already contains the preselected mutuals and the precalculated count.

{% comment %} my_template.html {% endcomment %}
{% for person in people %}
  <p>{{ person.account }}</p>
  {% for peer in person.mutual_peers %}
    <img src="{{ peer.avatar.url }}" class="hover-followers-img" />
  {% endfor %}
  <b>{{ person.mutual_total }}</b>
{% endfor %}

Alternative for Django 2.2

If you are on Django 2.2, prefetching with Exists directly in filter is not supported the same way. Use annotate to mark rows and then filter on that marker inside the Prefetch queryset.

# views.py
from django.db.models import Count, Exists, OuterRef, Prefetch, Q
from django.db.models.functions import Greatest
from django.shortcuts import render
def network_view(request):
    page_title = 'Following'
    people = (
        Persona.objects
        .exclude(Q(account=request.user))
        .annotate(
            mutual_total=Greatest(
                Count(
                    'follows',
                    filter=Q(follows__backers__account=request.user)
                ) - 2,
                0,
            )
        )
        .prefetch_related(
            Prefetch(
                'follows',
                Persona.objects
                .annotate(
                    is_peer=Exists(
                        Persona.follows.through.objects.filter(
                            from_persona_id=OuterRef('pk'),
                            to_persona__account=request.user,
                        )
                    )
                )
                .filter(is_peer=True),
                to_attr='mutual_peers',
            )
        )
    )
    return render(
        request,
        'my_template.html',
        {
            'people': people,
        },
    )

Rendering in the template is identical to the previous variant because the context shape is the same.

Why this matters

Counting and filtering in the template mixes concerns and hurts performance. Every in check in the loop hits the database or processes larger in-memory sets than necessary. Moving selection and aggregation to the queryset unlocks database-side optimizations, reduces memory usage, and makes templates clean and predictable. It also prevents confusing mistakes like trying to call count on a single object rather than on a collection.

Takeaways

Keep selection and aggregation in the view or queryset, not in templates. Prefetch only the mutual followers you plan to render, and annotate the count you want to show. Use snake_case for function names. If you are on an old Django release such as 2.2, prefer the annotate-then-filter pattern inside Prefetch; upgrading is recommended because newer releases improve ORM capabilities.

The article is based on a question from StackOverflow by user30880337 and an answer by willeM_ Van Onsem.