本文へ移動
cccskills
無料GitHub で公開

django-filter

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

インストール方法を見る

含まれるファイル(1)

  • SKILL.md17.7 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

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

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

同じリポジトリのスキル

概要と使いどころ

aiohttp

無料

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

日本語の概要は準備中です。原文の説明を表示しています。

CodeAtCode/oss-ai-skills222026年10月9日 更新

ast-grep

無料

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

日本語の概要は準備中です。原文の説明を表示しています。

CodeAtCode/oss-ai-skills222026年10月9日 更新

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

日本語の概要は準備中です。原文の説明を表示しています。

CodeAtCode/oss-ai-skills222026年10月9日 更新

celery

無料

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

日本語の概要は準備中です。原文の説明を表示しています。

CodeAtCode/oss-ai-skills222026年10月9日 更新

django

無料

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

日本語の概要は準備中です。原文の説明を表示しています。

CodeAtCode/oss-ai-skills222026年10月9日 更新

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

日本語の概要は準備中です。原文の説明を表示しています。

CodeAtCode/oss-ai-skills222026年10月9日 更新

CodeAtCode のスキルをすべて見る

このスキルの問題を報告する