Skip to content
cccskills
FreePublished on GitHub

django-filter

Use when filtering Django querysets - FilterSet, custom filters, Django REST Framework integration, explicit fields

The English summary is being prepared. Showing the original description.

See how to install

Files included (1)

  • SKILL.md17.7 KB

SKILL.md (original)

Review the instructions the agent will receive before you install.

django-filter

Django filtering library for dynamically filtering querysets, with full Django REST Framework integration.

Overview

django-filter provides a declarative way to filter querysets based on URL query parameters.

  • Declarative - Define filters as Python classes
  • DRF Integration - Seamless Django REST Framework support
  • Flexible - Custom filter backends and fields
  • Auto-generation - FilterSet from Django models

Installation

pip install django-filter

# With DRF integration
pip install django-filter djangorestframework

Add to INSTALLED_APPS:

INSTALLED_APPS = [
    'django_filters',
]

Basic FilterSet Pattern

# filters.py
import django_filters
from .models import Product

class ProductFilter(django_filters.FilterSet):
    # Range filters (min/max naming convention)
    price_min = django_filters.NumberFilter(field_name='price', lookup_expr='gte')
    price_max = django_filters.NumberFilter(field_name='price', lookup_expr='lte')
    
    # Text search
    name = django_filters.CharFilter(field_name='name', lookup_expr='icontains')
    
    # Boolean filter
    in_stock = django_filters.BooleanFilter(method='filter_in_stock')
    
    # Multiple selection (comma-separated values)
    categories = django_filters.CharFilter(field_name='category__slug', lookup_expr='in')
    
    # Date range
    created_after = django_filters.DateFilter(field_name='created_at', lookup_expr='gte')
    
    # Ordering
    order_by = django_filters.OrderingFilter(
        fields=[('price', 'price'), ('created_at', 'created_at'), ('name', 'name')]
    )

    class Meta:
        model = Product
        fields = '__all__'  # Django 6.0+: must be string '__all__' or explicit list

    def filter_in_stock(self, queryset, name, value):
        if value:
            return queryset.filter(stock__gt=0)
        return queryset

    class Meta:
        model = Product
        fields = ['category', 'name', 'price', 'stock']

Django 6.0 Breaking Change

The fields attribute requires explicit __all__:

class UserFilter(FilterSet):
    class Meta:
        model = User
        fields = '__all__'  # String, not list

Using fields = [] without __all__ raises an error in Django 6.0+.


Filter Type Selection

Choose filter types based on query shape, not field type. See django-filter field documentation for the full API.

Query shapeFilter typeExample
Exact matchCharFilter / NumberFiltercategory = CharFilter()
Multiple valuesCharFilter with lookup_expr='in'ids = CharFilter(lookup_expr='in')
Range (min/max)Two separate filtersprice_min, price_max
Date rangeDateFromToRangeFilterdate_range = DateFromToRangeFilter()
Relational (FK/M2M)Filter on related fieldauthor__name = CharFilter()
Custom logicFilter with method=status = Filter(method='filter_status')

Avoid enumerating all 12+ filter types — focus on the lookup expressions you actually need: exact, icontains, gt, gte, lt, lte, in, isnull.


Composable QuerySet Methods

When filtering is driven by code paths rather than user-supplied query params, define a custom QuerySet with one method per WHERE clause. The view reads like a sentence, and each method is independently testable.

from django.db import models
from django.utils import timezone

class EventQuerySet(models.QuerySet):
    def approved(self):
        return self.filter(approved_at__isnull=False)

    def future(self):
        today = timezone.localdate()
        return self.filter(end__gt=self._midnight(today))

    def _midnight(self, date):
        return timezone.datetime(date.year, date.month, date.day, tzinfo=timezone.utc)

    def with_tags(self, tags):
        if not tags:
            return self
        return self.filter(tags__name__in=tags).distinct()

    def is_free(self, free):
        if free is None:
            return self
        return self.filter(price=0) if free else self

    def for_tab(self, tab):
        if tab == 'featured':
            return self.filter(featured=True)
        if tab == 'popular':
            return self.filter(views__gte=100)
        return self

class Event(models.Model):
    objects = EventQuerySet.as_manager()
    # ...

Usage in a view — each method returns a queryset, so they chain naturally:

def event_list(request, tab):
    qs = (
        Event.objects.approved()
        .for_tab(tab)
        .with_tags(request.GET.getlist('tags'))
        .is_free(request.GET.get('free') == 'true')
    )
    return render(request, 'events/list.html', {'events': qs})

Guidelines for QuerySet methods

  • Return self (not None) when the filter is a no-op, so chains never break.
  • Guard each method against its argument being empty/None — callers should not have to branch before calling.
  • Keep methods single-purpose; compose in the view rather than adding flags.

FilterSet Composition

Combining FilterSets with &

Combine multiple FilterSet classes to create composite filters:

class BaseProductFilter(django_filters.FilterSet):
    """Common filters used across multiple views."""
    search = django_filters.CharFilter(method='filter_search')
    category = django_filters.ModelChoiceFilter(
        field_name='category',
        queryset=Category.objects.active()
    )

    class Meta:
        model = Product
        fields = []

    def filter_search(self, queryset, name, value):
        if not value:
            return queryset
        return queryset.filter(
            models.Q(name__icontains=value) |
            models.Q(description__icontains=value)
        )

class PriceFilter(django_filters.FilterSet):
    price_min = django_filters.NumberFilter(field_name='price', lookup_expr='gte')
    price_max = django_filters.NumberFilter(field_name='price', lookup_expr='lte')
    on_sale = django_filters.BooleanFilter(method='filter_on_sale')

    class Meta:
        model = Product
        fields = []

    def filter_on_sale(self, queryset, name, value):
        if value:
            return queryset.filter(discount__gt=0)
        return queryset

# Combine filters
ProductFilter = BaseProductFilter & PriceFilter

Failure mode: Without composition, you either duplicate filter definitions or create bloated monolithic FilterSet classes. The & operator merges filters cleanly.

Subclassing for shared base filters

class BaseFilter(django_filters.FilterSet):
    """Base class with filters shared across multiple FilterSets."""
    created_after = django_filters.DateFilter(field_name='created_at', lookup_expr='gte')
    created_before = django_filters.DateFilter(field_name='created_at', lookup_expr='lte')
    ordering = django_filters.OrderingFilter(
        fields=['created_at', 'updated_at', 'name']
    )

    class Meta:
        model = None  # Subclasses must define Meta.model
        fields = []

class ProductFilter(BaseFilter):
    class Meta:
        model = Product
        fields = ['category', 'is_active']

class OrderFilter(BaseFilter):
    class Meta:
        model = Order
        fields = ['status', 'customer']

Failure mode: Duplicating date range and ordering filters across multiple FilterSet classes leads to drift when you add a new field to one but forget another.

Using @property for computed filters

class ProductFilter(django_filters.FilterSet):
    price_min = django_filters.NumberFilter()
    price_max = django_filters.NumberFilter()

    class Meta:
        model = Product
        fields = []

    @property
    def is_price_range_valid(self):
        """Check if both min and max are provided and valid."""
        price_min = self.data.get('price_min')
        price_max = self.data.get('price_max')
        if price_min and price_max:
            return float(price_min) <= float(price_max)
        return True

    @property
    def qs(self):
        qs = super().qs
        # Access computed property to trigger validation
        if not self.is_price_range_valid:
            # Return empty queryset or raise error
            return qs.none()
        return qs

Failure mode: Trying to validate cross-field constraints in __init__ fails because filters haven't been instantiated yet. Use @property on qs to validate after filter application.

Overriding get_queryset() for cross-field logic

class ProductFilter(django_filters.FilterSet):
    min_price = django_filters.NumberFilter(field_name='price', lookup_expr='gte')
    max_price = django_filters.NumberFilter(field_name='price', lookup_expr='lte')
    category = django_filters.CharFilter()

    class Meta:
        model = Product
        fields = []

    def get_queryset(self):
        """Access other filter values for cross-field logic."""
        qs = super().get_queryset()
        
        # Example: filter based on relationship between fields
        min_price = self.data.get('min_price')
        max_price = self.data.get('max_price')
        
        if min_price and max_price:
            # Custom logic: ensure price is within 10% of midpoint
            midpoint = (float(min_price) + float(max_price)) / 2
            tolerance = midpoint * 0.1
            qs = qs.filter(price__gte=midpoint - tolerance, price__lte=midpoint + tolerance)
        
        return qs

Failure mode: FilterSet re-runs get_queryset() per request — there's no caching of the base queryset. If your get_queryset() performs expensive operations, cache the result or move logic to the view.

distinct() for ManyToMany traversals

class ProductFilter(django_filters.FilterSet):
    tags = django_filters.CharFilter(field_name='tags__name', lookup_expr='in')
    categories = django_filters.CharFilter(field_name='category__slug', lookup_expr='in')

    class Meta:
        model = Product
        fields = ['tags', 'categories']
        # Enable distinct to avoid duplicate rows from M2M joins
        # Note: django-filter doesn't auto-add distinct; you must handle it

In the view:

def product_list(request):
    filter = ProductFilter(request.GET, queryset=Product.objects.all())
    qs = filter.qs.distinct()  # Required for M2M filters
    return render(request, 'products.html', {'products': qs})

Failure mode: Without distinct(), filtering on multiple M2M relationships produces duplicate rows (Cartesian product of joins).


Decision: django-filter vs plain query params vs manual filtering

Use django-filter when

  • You need DRF integration (automatic filter schema, OpenAPI docs)
  • Filters are user-facing and dynamic (users can combine any filter)
  • You have 10+ filter parameters or complex cross-field logic
  • Multiple views share the same filter set
  • You want validation of filter values (type coercion, range checks)

Use plain request.GET when

  • You have 2-3 stable parameters (e.g., page, limit, sort)
  • The API is internal with controlled consumers
  • You need full control over query construction
  • The threshold: hand-written request.GET handling is simpler for ≤3 params
def product_list(request):
    page = int(request.GET.get('page', 1))
    limit = min(int(request.GET.get('limit', 20)), 100)  # Cap at 100
    sort = request.GET.get('sort', 'created_at')
    
    if sort not in ['created_at', 'price', 'name']:
        sort = 'created_at'  # Whitelist validation
    
    qs = Product.objects.all().order_by(sort)[(page-1)*limit:page*limit]
    return render(request, 'products.html', {'products': qs})

When django-filter is overkill

  • Public API with a handful of stable params — manual handling is clearer
  • Filters that depend on session/auth state — put logic in get_queryset()
  • Complex business logic — use QuerySet methods instead of filters

Correctness & Performance

distinct() on ManyToMany traversals

When filtering on M2M relationships, JOINs produce duplicate rows:

# Without distinct: products with 3 tags appear 3 times
qs = Product.objects.filter(tags__name__in=['sale', 'new'])

# With distinct: each product appears once
qs = Product.objects.filter(tags__name__in=['sale', 'new']).distinct()

Warning: distinct() without arguments uses all fields. If you need to order by a filtered field, use distinct('field_name') (PostgreSQL only) or restructure the query.

Indexing filtered columns

A filter on an unindexed column is a table scan:

class Product(models.Model):
    name = models.CharField(max_length=255, db_index=True)  # Index on filter field
    price = models.DecimalField(max_digits=10, 2, db_index=True)
    category = models.ForeignKey(Category, on_delete=models.CASCADE)
    
    class Meta:
        indexes = [
            models.Index(fields=['-created_at']),  # For ordering
            models.Index(fields=['category', '-created_at']),  # Composite
        ]

Use EXPLAIN to verify index usage on slow filters.

Bounding __in with user-supplied lists

# Dangerous: unbounded list from user input
ids = request.GET.getlist('ids')
Product.objects.filter(id__in=ids)  # Could be millions of rows

# Safe: cap the list length
MAX_IDS = 100
ids = request.GET.getlist('ids')[:MAX_IDS]
Product.objects.filter(id__in=ids)

Failure mode: id__in with 10,000 values creates a massive SQL IN clause, causing query timeouts.

FilterSet re-runs get_queryset() per request

class ProductViewSet(viewsets.ModelViewSet):
    filterset_class = ProductFilter
    
    def get_queryset(self):
        # This runs EVERY request, not just once
        return Product.objects.select_related('category')

Failure mode: Assuming get_queryset() is cached and putting expensive operations there (e.g., calling an external API). Cache explicitly if needed:

from django.core.cache import cache

def get_queryset(self):
    cache_key = 'product_base_qs'
    qs = cache.get(cache_key)
    if qs is None:
        qs = Product.objects.select_related('category')
        cache.set(cache_key, qs, timeout=300)
    return qs

Django REST Framework Integration

Basic ViewSet setup

from rest_framework import viewsets
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import filters
from .models import Product
from .filters import ProductFilter

class ProductViewSet(viewsets.ModelViewSet):
    queryset = Product.objects.all()
    serializer_class = ProductSerializer
    
    filter_backends = [DjangoFilterBackend, filters.OrderingFilter]
    filterset_class = ProductFilter
    ordering_fields = ['price', 'created_at']
    ordering = ['-created_at']

Ordering pitfall: OrderingFilter vs FilterSet ordering

OrderingFilter (from DRF) and FilterSet ordering are separate:

class ProductFilter(django_filters.FilterSet):
    # This creates an 'order_by' filter param
    order_by = django_filters.OrderingFilter(
        fields=['price', 'created_at']
    )

    class Meta:
        model = Product
        fields = []

class ProductViewSet(viewsets.ModelViewSet):
    filter_backends = [DjangoFilterBackend, filters.OrderingFilter]
    filterset_class = ProductFilter
    # ordering_fields applies to DRF's OrderingFilter, NOT FilterSet
    ordering_fields = ['price', 'created_at']

Failure mode: Using both OrderingFilter in filter_backends AND an OrderingFilter field in your FilterSet creates conflicting ordering behavior. Pick one:

  • Use FilterSet.ordering for filter-param-based ordering (user types ?order_by=-price)
  • Use OrderingFilter in filter_backends for header-based ordering (user sends Order: -price)

URL Patterns

# Basic filtering
GET /api/products/?category=1
GET /api/products/?price_min=10&price_max=100

# Text search
GET /api/products/?name__icontains=laptop

# Multiple values (comma-separated)
GET /api/products/?categories=electronics,books

# Date range
GET /api/products/?created_after=2024-01-01

# Ordering
GET /api/products/?order_by=-price
GET /api/products/?ordering=-price  # DRF OrderingFilter

Common Pitfalls

S silently ignoring unknown query parameters

django-filter ignores parameters not defined in your FilterSet:

class ProductFilter(django_filters.FilterSet):
    price_min = django_filters.NumberFilter()
    
    class Meta:
        model = Product
        fields = []

# GET /api/products/?unknown_param=123 → silently ignored

Fix: Validate with strict=True (django-filter 4.0+):

class ProductFilter(django_filters.FilterSet):
    class Meta:
        model = Product
        fields = []
        strict = True  # Raise error on unknown params

Not exposing all model fields accidentally

class Meta:
    model = Product
    fields = '__all__'  # Exposes EVERY field — often unintended

Fix: Be explicit:

class Meta:
    model = Product
    fields = ['category', 'price', 'name', 'is_active']

References

Reviews

No reviews yet. Tell others what you thought.

Comments

No login needed. Share how it worked for you, ask a question, or note where it did not run.

No comments yet. Be the first to write one.

Comments are published right away. Do not include personal information such as email addresses or phone numbers. Up to 2 URLs.

0 / 1,000 characters

This form is protected by reCAPTCHA, and the Google Privacy Policy and Terms of Service apply.

More skills from this repository

Use when building Python async HTTP services or clients with aiohttp - web server routing, middleware, WebSocket, SSE, streaming, client sessions, pytest-aiohttp testing, or troubleshooting SSL and timeout issues

The English summary is being prepared. Showing the original description.

CodeAtCode/oss-ai-skills24Updated Oct 9, 2026

Use when doing structural code search and rewriting - ast-grep linting, refactoring, multi-language patterns

The English summary is being prepared. Showing the original description.

CodeAtCode/oss-ai-skills24Updated Oct 9, 2026

Use when building GBA games with the BPCore Lua engine - entity, sprite and tilemap functions, SRAM save and load, link cable multiplayer protocol, camera and scrolling, or optimization patterns

The English summary is being prepared. Showing the original description.

CodeAtCode/oss-ai-skills24Updated Oct 9, 2026

celery

Free

Use when running background tasks with Celery - worker and broker configuration (Redis, RabbitMQ), task routing by name vs queue, chains/groups/chords, retry patterns (autoretry_for, retry_backoff), acks_late semantics, failure detection, and monitoring with Flower

The English summary is being prepared. Showing the original description.

CodeAtCode/oss-ai-skills24Updated Oct 9, 2026

django

Free

Use when building Django applications - security hardening, authentication and permissions, ORM optimization, PostgreSQL features, Django 6.0, migrations, testing, and ecosystem libraries

The English summary is being prepared. Showing the original description.

CodeAtCode/oss-ai-skills24Updated Oct 9, 2026

Use when customizing Django Admin - save_formset, get_search_results, formsets, queryset optimization, db_index, custom URLs

The English summary is being prepared. Showing the original description.

CodeAtCode/oss-ai-skills24Updated Oct 9, 2026

See all skills by CodeAtCode

Report a problem with this skill