Django templates are deliberately bad at being a query language. That is a feature, not a missing capability. If a template decides which rows to load, applies business rules, and then walks relationships one item at a time, the result is difficult to test and surprisingly easy to turn into an N+1 query problem.
The useful question is not, "How do I filter a queryset in a Django template?" It is, "Which layer should own this decision, and how do I give the template exactly what it needs to render?" For most applications, the answer is a view, a small query function, or a model manager. The template should receive prepared data and make presentation decisions only.
Start With a Queryset That States the Page's Intent
Suppose a product listing should show currently available products. Put that rule where a reader of the view can see it immediately:
# views.py
from django.shortcuts import render
from .models import Product
def product_list(request):
products = Product.objects.filter(is_available=True).order_by("name")
return render(request, "catalog/product_list.html", {"products": products})
That is not just tidier than calling .filter() from a custom template filter. It
also gives you one obvious place to add authorization, ordering, caching, eager
loading, and tests. The template gets a collection whose meaning is already clear:
{% for product in products %}
<li>{{ product.name }}</li>
{% empty %}
<li>No products are available right now.</li>
{% endfor %}
For a rule used by more than one page, move the rule closer to the model. A custom
QuerySet keeps chaining available and avoids an ever-growing pile of one-off
manager methods:
# models.py
from django.db import models
class ProductQuerySet(models.QuerySet):
def available(self):
return self.filter(is_available=True)
class Product(models.Model):
name = models.CharField(max_length=200)
is_available = models.BooleanField(default=True)
category = models.ForeignKey("Category", on_delete=models.PROTECT)
objects = ProductQuerySet.as_manager()
Now a view can say Product.objects.available() without teaching the template
anything about persistence. This is also a good boundary for rules such as
"visible to this account" or "not archived." Keep request-specific policy out of
the model when it needs the current user or organization; a query function or
service layer is usually clearer in that case.
Load Relationships Before Rendering
The most expensive template bugs often look harmless. This loop can issue one
extra query per product if category has not been loaded:
{% for product in products %}
<li>{{ product.name }} — {{ product.category.name }}</li>
{% endfor %}
Use select_related() for a foreign key or one-to-one relationship that the page
will display:
products = (
Product.objects.available()
.select_related("category")
.order_by("name")
)
Use prefetch_related() for many-to-many and reverse relationships. For example,
if every product card shows its labels:
products = (
Product.objects.available()
.select_related("category")
.prefetch_related("labels")
.order_by("name")
)
These are performance decisions, so they belong next to the query rather than inside a template tag. Use Django's query logging or a development tool to check the count with realistic page sizes. Premature eager loading can waste memory, but rendering a list without noticing repeated relationship lookups is usually worse.
Treat User-Supplied Filters as Input, Not Template Logic
Search and filtering pages often receive their state through query parameters:
?category=hardware&q=keyboard. A GET form makes the state linkable and
refreshable. A Django form validates it before it reaches the ORM.
# forms.py
from django import forms
class ProductFilterForm(forms.Form):
q = forms.CharField(required=False, max_length=100)
category = forms.IntegerField(required=False, min_value=1)
# views.py
from django.core.paginator import Paginator
from django.shortcuts import render
from .forms import ProductFilterForm
from .models import Product
def product_list(request):
form = ProductFilterForm(request.GET)
products = Product.objects.available().select_related("category")
if form.is_valid():
query = form.cleaned_data["q"]
category_id = form.cleaned_data["category"]
if query:
products = products.filter(name__icontains=query)
if category_id:
products = products.filter(category_id=category_id)
page = Paginator(products.order_by("name"), 25).get_page(request.GET.get("page"))
return render(request, "catalog/product_list.html", {"form": form, "page": page})
The form does not make a search page magically safe or fast. It does make the accepted inputs explicit, and it keeps invalid values from silently becoming a different query. For more involved filtering, a dedicated filter object can compose the query a step at a time and be tested without rendering HTML.
Paginate the Queryset, Not a Python List
Paginator works best with a queryset because Django can add LIMIT and
OFFSET rather than loading every matching row into Python. Do not turn a large
queryset into list(products) just to make template filtering convenient.
The template then has a compact, predictable job:
<form method="get">
{{ form.q }}
{{ form.category }}
<button type="submit">Filter</button>
</form>
{% for product in page %}
<article>
<h2>{{ product.name }}</h2>
<p>{{ product.category.name }}</p>
</article>
{% empty %}
<p>No products matched those filters.</p>
{% endfor %}
{% if page.has_other_pages %}
<nav aria-label="Product pages">
{% if page.has_previous %}
<a href="?page={{ page.previous_page_number }}">Newer results</a>
{% endif %}
<span>Page {{ page.number }} of {{ page.paginator.num_pages }}</span>
{% if page.has_next %}
<a href="?page={{ page.next_page_number }}">Older results</a>
{% endif %}
</nav>
{% endif %}
In a production filter UI, preserve the other query parameters when building pagination links. A small template tag for URL manipulation is fine because it formats request state; it should not decide which database rows exist.
What Template Filters Are Actually Good For
Template filters are useful for presentation-only transformations: formatting a date, choosing a human-friendly label, or rendering a value consistently. They should be deterministic and should not hit the database.
# templatetags/product_display.py
from django import template
register = template.Library()
@register.filter
def availability_label(is_available):
return "In stock" if is_available else "Unavailable"
{% load product_display %}
<span>{{ product.is_available|availability_label }}</span>
A filter named available_products that calls products.filter(...) is tempting,
but it hides query construction in a rendering layer. It becomes especially risky
when another developer reuses it inside a loop or assumes the input has already
been evaluated. The same concern applies to template tags that retrieve models by
default. Make database access obvious in Python code that can be profiled and
covered by tests.
A Practical Ownership Rule
Use this simple division of responsibilities:
- Model queryset or manager: reusable, data-centric rules that do not need the request.
- View or query/service function: request-specific filters, authorization, ordering, eager loading, and pagination.
- Form: validation and normalization of user-controlled filter values.
- Template: loops, empty states, and display formatting.
That division makes a Django page easier to reason about when the first version
becomes the fifth. It also makes performance work concrete: inspect the query in
one place, add select_related() or prefetch_related() where it belongs, and
verify the result with a test. For another example of keeping Django behavior
explicit rather than relying on template magic, see how to limit a Django
ForeignKey to staff users.
Filtering belongs where the page's data contract is built. Once the template gets a small, already-prepared queryset or page object, it can do what templates are good at: make the result understandable to a human.
For more practical software-engineering guidance, return to Slaptijack.