Agent Skills: django-filter

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

UncategorizedID: CodeAtCode/oss-ai-skills/django-filter

Install this agent skill to your local

pnpm dlx add-skill https://github.com/CodeAtCode/oss-ai-skills/tree/HEAD/frameworks/django-filter

Skill Files

Browse the full folder contents for django-filter.

Download Skill

Loading file tree…

frameworks/django-filter/SKILL.md

Skill Metadata

Name
django-filter
Description
Use when filtering Django querysets - FilterSet, custom filters, Django REST Framework integration, explicit fields

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 support
pip install django-filter djangorestframework

Add to INSTALLED_APPS:

INSTALLED_APPS = [
    # ...
    'django_filters',
]

Basic Usage

Django 6.0 Breaking Change

fields requires explicit __all__:

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

Using fields = [] or fields = ['field1', 'field2'] without __all__ will raise an error in Django 6.0+

FilterSet Class

# filters.py
import django_filters
from .models import Product

class ProductFilter(django_filters.FilterSet):
    # Exact match
    category = django_filters.NumberFilter(field_name='category_id')
    
    # Lookup expressions (icontains, exact, gt, gte, lt, lte, contains, in)
    name = django_filters.CharFilter(field_name='name', lookup_expr='icontains')
    price_min = django_filters.NumberFilter(field_name='price', lookup_expr='gte')
    price_max = django_filters.NumberFilter(field_name='price', lookup_expr='lte')
    
    # Boolean filter
    in_stock = django_filters.BooleanFilter(field_name='stock', lookup_expr='gt', method='filter_in_stock')
    
    # Date filters
    created_after = django_filters.DateFilter(field_name='created_at', lookup_expr='gte')
    created_before = django_filters.DateFilter(field_name='created_at', lookup_expr='lte')
    
    # Multiple selection (comma-separated)
    categories = django_filters.CharFilter(field_name='category_id', lookup_expr='in')
    
    # Ordering filter
    order_by = django_filters.OrderingFilter(
        fields=['price', 'created_at', 'name'],
        field_labels={'price': 'Price', 'created_at': 'Date'}
    )

    class Meta:
        model = Product
        # Django 6.0+: Use '__all__' string or explicit field list
        fields = '__all__'  # or ['category', 'name', 'price', ...]

    def filter_in_stock(self, queryset, name, value):
        """Custom filter method for in_stock."""
        if value:
            return queryset.filter(stock__gt=0)
        return queryset.filter(stock=0)

FilterSet with ModelForm

# Auto-generate filters from model fields
class ProductFilter(django_filters.FilterSet):
    class Meta:
        model = Product
        fields = ['name', 'category', 'price', 'is_active', 'stock']

Django REST Framework Integration

ViewSet Integration

# views.py
from rest_framework import viewsets
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import filters
from .models import Product
from .serializers import ProductSerializer
from .filters import ProductFilter

class ProductViewSet(viewsets.ModelViewSet):
    queryset = Product.objects.all()
    serializer_class = ProductSerializer
    
    # Filter backends
    filter_backends = [
        DjangoFilterBackend,
        filters.SearchFilter,
        filters.OrderingFilter
    ]
    
    # Filterset class
    filterset_class = ProductFilter
    
    # Or inline filterset_fields
    filter_fields = ['category', 'is_active']
    
    # Search configuration
    search_fields = ['name', 'description', 'category__name']
    
    # Ordering fields
    ordering_fields = ['price', 'created_at', 'name']
    ordering = ['-created_at']

APIView Integration

# views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import filters
from .models import Product
from .serializers import ProductSerializer
from .filters import ProductFilter

class ProductListView(APIView):
    def get(self, request):
        queryset = Product.objects.all()
        
        # Apply filters manually
        filter_backend = DjangoFilterBackend()
        queryset = filter_backend.filter_queryset(request, queryset, self)
        
        serializer = ProductSerializer(queryset, many=True)
        return Response(serializer.data)

Filter Types Reference

NumberFilter

price = django_filters.NumberFilter()
price_gt = django_filters.NumberFilter(field_name='price', lookup_expr='gt')
price_range = django_filters.RangeFilter(field_name='price')

CharFilter

name = django_filters.CharFilter(lookup_expr='icontains')
description = django_filters.CharFilter(lookup_expr='contains')

DateFilter / DateTimeFilter

created = django_filters.DateFilter()
created_after = django_filters.DateTimeFilter(field_name='created_at', lookup_expr='gte')

BooleanFilter

is_active = django_filters.BooleanFilter()
is_featured = django_filters.BooleanFilter(field_name='is_featured', distinct=False)

ModelChoiceFilter / ModelMultipleChoiceFilter

category = django_filters.ModelChoiceFilter(
    queryset=Category.objects.all(),
    empty_label='All Categories'
)

tags = django_filters.ModelMultipleChoiceFilter(
    queryset=Tag.objects.all(),
    conjoined=True  # AND vs OR behavior
)

ChoiceFilter

status = django_filters.ChoiceFilter(
    choices=Product.STATUS_CHOICES,
    empty_label=None
)

UUIDFilter

id = django_filters.UUIDFilter(field_name='id')

AllValuesFilter / AllValuesMultipleFilter

# Dropdown with all possible values
category = django_filters.AllValuesFilter(field_name='category_id')

DateFromToRangeFilter

date_range = django_filters.DateFromToRangeFilter(field_name='created_at')

TimeFromToRangeFilter

time_range = django_filters.TimeFromToRangeFilter(field_name='created_at')

DateTimeFromToRangeFilter

datetime_range = django_filters.DateTimeFromToRangeFilter(field_name='created_at')

NumberRangeFilter

price_range = django_filters.NumberRangeFilter(field_name='price')

Custom Filter Methods

Method Filter with Request Context

class ProductFilter(django_filters.FilterSet):
    # Access request in filter method
    category = django_filters.NumberFilter(method='filter_by_category')
    
    class Meta:
        model = Product
        fields = ['category']

    def filter_by_category(self, queryset, name, value):
        # Access request, user, etc. via self.request
        user = self.request.user
        
        if user.is_premium:
            return queryset.filter(category_id=value)
        return queryset.filter(category_id=value, category__is_premium=False)

Filter with Multiple Fields

class ProductFilter(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')
    min_max_price = django_filters.NumberFilter(method='filter_price_range')

    def filter_price_range(self, queryset, name, value):
        # Handle custom filter logic
        return queryset.filter(price__gte=value)

Composable QuerySet Methods (FilterSet Alternative)

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

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 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

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_festivals(tab_params.festival_slugs)
        .is_free(tab_params.free)
    )
    return render(request, 'events/list.html', {'events': qs})

When to choose QuerySet methods vs FilterSet

| Need | Pick | |------|------| | User-facing filters from query params, DRF integration, auto-generated form | FilterSet | | Code-driven filtering, readable chains, no query-param binding | Custom QuerySet methods | | Both | Combine: FilterSet for the API surface, QuerySet methods for internal reuse |

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.

DRF Tips & Patterns

FilterBackend per Action

class ProductViewSet(viewsets.ModelViewSet):
    def get_filter_backends(self):
        if self.action == 'list':
            return [DjangoFilterBackend, filters.SearchFilter]
        return []

class OrderViewSet(viewsets.ModelViewSet):
    filter_backends = [DjangoFilterBackend]
    filterset_class = OrderFilter
    
    def get_queryset(self):
        # Exclude cancelled orders for non-admin users
        if not self.request.user.is_staff:
            return Order.objects.exclude(status='cancelled')
        return Order.objects.all()

Conditional Filter Fields

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

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        
        # Add fields dynamically based on user
        if self.request.user.is_staff:
            self.filters['is_active'].field_class = django_filters.BooleanFilter()

Select Related in Filtered Viewsets

class ProductViewSet(viewsets.ModelViewSet):
    serializer_class = ProductSerializer
    
    def get_queryset(self):
        # Use select_related to avoid N+1
        return Product.objects.select_related('category').prefetch_related('tags')

URL Patterns

Basic

GET /api/products/
GET /api/products/?category=1
GET /api/products/?category=1&price_min=10&price_max=100
GET /api/products/?search=laptop
GET /api/products/?ordering=price
GET /api/products/?ordering=-price
GET /api/products/?categories=1,2,3

With Filterset Class

GET /api/products/?name__icontains=laptop
GET /api/products/?price__gte=10
GET /api/products/?in_stock=true
GET /api/products/?created_after=2024-01-01

Range Filters

GET /api/products/?price_range_min=10&price_range_max=100
GET /api/products/?date_range_after=2024-01-01&date_range_before=2024-12-31

Best Practices

Filter Naming Conventions

# Use descriptive names that map to query params
price_min = django_filters.NumberFilter(field_name='price', lookup_expr='gte')
price_max = django_filters.NumberFilter(field_name='price', lookup_expr='lte')

# In URL: ?price_min=10&price_max=100

Performance

# Always use select_related/prefetch_related in get_queryset
class ProductViewSet(viewsets.ModelViewSet):
    def get_queryset(self):
        return Product.objects.select_related('category', 'vendor').prefetch_related('tags')

Validation

class ProductFilter(django_filters.FilterSet):
    price_min = django_filters.NumberFilter()
    price_max = django_filters.NumberFilter()
    
    class Meta:
        model = Product
        fields = ['price_min', 'price_max']
    
    def validate_price_range(self, cleaned_data):
        if cleaned_data.get('price_min') and cleaned_data.get('price_max'):
            if cleaned_data['price_min'] > cleaned_data['price_max']:
                raise serializers.ValidationError("price_min must be less than price_max")
        return cleaned_data

Do

  • Use filterset_class for complex filtering logic
  • Use select_related and prefetch_related to avoid N+1 queries
  • Use meaningful filter field names (price_min, price_max instead of price used twice)
  • Add empty labels for optional filters with ModelChoiceFilter

Don't

  • Don't expose all model fields as filters - only expose what's needed
  • Don't forget to add appropriate indexes on filtered fields
  • Don't use filters that require expensive operations without pagination

References

  • django-filter Docs: https://django-filter.readthedocs.io/en/stable/guide/usage.html
  • DRF Integration: https://django-filter.readthedocs.io/en/stable/guide/rest_framework.html
  • Tips: https://django-filter.readthedocs.io/en/stable/guide/tips.html
  • GitHub: https://github.com/carltongibson/django-filter
  • jvns.ca – More nice Django things (composable QuerySet methods): https://jvns.ca/blog/2026/07/21/more-nice-django-things/