Tips and Solutions

Common problems for declared filters

Below are some of the common problem that occur when declaring filters. It is recommended that you read this as it provides a more complete understanding of how filters work.

Filter name and lookup_expr not configured

While name and lookup_expr are optional, it is recommended that you specify them. By default, if name is not specified, the filter’s name on the filterset class will be used. Additionally, lookup_expr defaults to exact. The following is an example of a misconfigured price filter:

class ProductFilter(django_filters.FilterSet):
    price__gt = django_filters.NumberFilter()

The filter instance will have a field name of price__gt and an exact lookup type. Under the hood, this will incorrectly be resolved as:

Produce.objects.filter(price__gt__exact=value)

The above will most likely generate a FieldError. The correct configuration would be:

class ProductFilter(django_filters.FilterSet):
    price__gt = django_filters.NumberFilter(name='price', lookup_expr='gt')

Missing lookup_expr for text search filters

It’s quite common to forget to set the lookup expression for CharField and TextField and wonder why a search for “foo” does not return results for “foobar”. This is because the default lookup type is exact, but you probably want to perform an icontains lookup.

Filter and lookup expression mismatch (in, range, isnull)

It’s not always appropriate to directly match a filter to its model field’s type, as some lookups expect different types of values. This is a commonly found issue with in, range, and isnull lookups. Let’s look at the following product model:

class Product(models.Model):
    category = models.ForeignKey(Category, null=True)

Given that category is optional, it’s reasonable to want to enable a search for uncategorized products. The following is an incorrectly configured isnull filter:

class ProductFilter(django_filters.FilterSet):
    uncategorized = django_filters.NumberFilter(name='category', lookup_expr='isnull')

So what’s the issue? While the underlying column type for category is an integer, isnull lookups expect a boolean value. A NumberFilter however only validates numbers. Filters are not ‘expression aware’ and won’t change behavior based on their lookup_expr. You should use filters that match the data type of the lookup expression instead of the data type underlying the model field. The following would correctly allow you to search for both uncategorized products and products for a set of categories:

class NumberInFilter(django_filters.BaseInFilter, django_filters.NumberFilter):
    pass

class ProductFilter(django_filters.FilterSet):
    categories = NumberInFilter(name='category', lookup_expr='in')
    uncategorized = django_filters.BooleanFilter(name='category', lookup_expr='isnull')

More info on constructing in and range csv filters.

Filtering by empty values

There are a number of cases where you may need to filter by empty or null values. The following are some common solutions to these problems:

Filtering by null values

As explained in the above “Filter and lookup expression mismatch” section, a common problem is how to correctly filter by null values on a field.

Solution 1: Using a BooleanFilter with isnull

Using BooleanFilter with an isnull lookup is a builtin solution used by the FilterSet’s automatic filter generation. To do this manually, simply add:

class ProductFilter(django_filters.FilterSet):
    uncategorized = django_filters.BooleanFilter(name='category', lookup_expr='isnull')

Note

Remember that the filter class is validating the input value. The underlying type of the mode field is not relevant here.

You may also reverse the logic with the exclude parameter.

class ProductFilter(django_filters.FilterSet):
    has_category = django_filters.BooleanFilter(name='category', lookup_expr='isnull', exclude=True)

Solution 2: Using ChoiceFilter‘s null choice

If you’re using a ChoiceFilter, you may also filter by null values by enabling the null_label parameter. More details in the ChoiceFilter reference docs.

class ProductFilter(django_filters.FilterSet):
    category = django_filters.ModelChoiceFilter(
        name='category', lookup_expr='isnull',
        null_label='Uncategorized',
        queryset=Category.objects.all(),
    )

Solution 3: Combining fields w/ MultiValueField

An alternative approach is to use Django’s MultiValueField to manually add in a BooleanField to handle null values. Proof of concept: https://github.com/carltongibson/django-filter/issues/446

Filtering by an empty string

It’s not currently possible to filter by an empty string, since empty values are interpreted as a skipped filter.

Solution 1: Magic values

You can override the filter() method of a filter class to specifically check for magic values. This is similar to the ChoiceFilter‘s null value handling.

class MyCharFilter(filters.CharFilter):
    empty_value = 'EMPTY'

    def filter(self, qs, value):
        if value != self.empty_value:
            return super(MyCharFilter, self).filter(qs, value)

        qs = self.get_method(qs)(**{'%s__%s' % (self.name, self.lookup_expr): ""})
        return qs.distinct() if self.distinct else qs

Solution 2: Empty string filter

It would also be possible to create an empty value filter that exhibits the same behavior as an isnull filter.

from django.core.validators import EMPTY_VALUES

class EmptyStringFilter(filters.BooleanFilter):
    def filter(self, qs, value):
        if value in EMPTY_VALUES:
            return qs

        exclude = self.exclude ^ (value is False)
        method = qs.exclude if exclude else qs.filter

        return method(**{self.name: ""})


class MyFilterSet(filters.FilterSet):
    myfield__isempty = EmptyStringFilter(name='myfield')

    class Meta:
        model = MyModel

Using initial values as defaults

In pre-1.0 versions of django-filter, a filter field’s initial value was used as a default when no value was submitted. This behavior was not officially supported and has since been removed.

Warning

It is recommended that you do NOT implement the below as it adversely affects usability. Django forms don’t provide this behavior for a reason.

  • Using initial values as defaults is inconsistent with the behavior of Django forms.
  • Default values prevent users from filtering by empty values.
  • Default values prevent users from skipping that filter.

If defaults are necessary though, the following should mimic the pre-1.0 behavior:

class BaseFilterSet(FilterSet):

    def __init__(self, data=None, *args, **kwargs):
        # if filterset is bound, use initial values as defaults
        if data is not None:
            # get a mutable copy of the QueryDict
            data = data.copy()

            for name, f in self.base_filters.items():
                initial = f.extra.get('initial')

                # filter param is either missing or empty, use initial as default
                if not data.get(name) and initial:
                    data[name] = initial

        super(BaseFilterSet, self).__init__(data, *args, **kwargs)