CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-djangorestframework-stubs

PEP 484 type stubs for Django REST Framework enabling static type checking with comprehensive type definitions for all major DRF components

Pending
Overview
Eval results
Files

pagination-filtering.mddocs/

Pagination & Filtering

Django REST Framework provides comprehensive pagination and filtering capabilities for managing large datasets in API responses. The type stubs ensure type-safe pagination implementations and filter backend configurations with full generic support.

Pagination Classes

BasePagination

class BasePagination:
    """Base class for all pagination styles."""
    
    display_page_controls: bool
    template: str | None
    
    def paginate_queryset(
        self,
        queryset: QuerySet[_MT],
        request: Request,
        view: APIView | None = None
    ) -> list[_MT] | None:
        """
        Paginate a queryset and return a page of results.
        
        Args:
            queryset: QuerySet to paginate
            request: Current request object
            view: View that initiated the pagination
            
        Returns:
            list[_MT] | None: Page of results or None if no pagination
        """
        ...
    
    def get_paginated_response(self, data: Any) -> Response:
        """
        Return paginated response with page metadata.
        
        Args:
            data: Serialized page data
            
        Returns:
            Response: Response with pagination metadata
        """
        ...
    
    def get_paginated_response_schema(self, schema: Any) -> dict[str, Any]:
        """Return schema for paginated responses."""
        ...
    
    def to_html(self) -> str:
        """Return HTML representation for browsable API."""
        ...

PageNumberPagination

class PageNumberPagination(BasePagination):
    """Paginate by page numbers (e.g., ?page=2)."""
    
    # Configuration attributes
    page_size: int
    page_query_param: str  # Default: 'page'
    page_size_query_param: str | None  # Default: None
    max_page_size: int | None  # Default: None
    last_page_strings: tuple[str, ...]  # Default: ('last',)
    template: str
    
    def __init__(self) -> None: ...
    
    def get_page_number(self, request: Request, paginator: Paginator) -> int: ...
    def get_page_size(self, request: Request) -> int: ...
    def get_next_link(self) -> str | None: ...
    def get_previous_link(self) -> str | None: ...
    def get_first_link(self) -> str | None: ...
    def get_last_link(self) -> str | None: ...

Configuration:

  • page_size: int - Default number of items per page
  • page_query_param: str - Query parameter name for page number
  • page_size_query_param: str | None - Query parameter for custom page size
  • max_page_size: int | None - Maximum allowed page size

LimitOffsetPagination

class LimitOffsetPagination(BasePagination):
    """Paginate using limit/offset parameters (e.g., ?limit=20&offset=40)."""
    
    # Configuration attributes
    default_limit: int | None
    limit_query_param: str  # Default: 'limit'
    offset_query_param: str  # Default: 'offset'
    max_limit: int | None
    template: str
    
    def __init__(self) -> None: ...
    
    def get_limit(self, request: Request) -> int: ...
    def get_offset(self, request: Request) -> int: ...
    def get_count(self, queryset: QuerySet[_MT]) -> int: ...
    def get_next_link(self) -> str | None: ...
    def get_previous_link(self) -> str | None: ...

Configuration:

  • default_limit: int | None - Default number of items per page
  • limit_query_param: str - Query parameter name for limit
  • offset_query_param: str - Query parameter name for offset
  • max_limit: int | None - Maximum allowed limit

CursorPagination

class CursorPagination(BasePagination):
    """Cursor-based pagination for consistent performance with large datasets."""
    
    # Configuration attributes
    page_size: int
    page_size_query_param: str | None
    max_page_size: int | None
    cursor_query_param: str  # Default: 'cursor'
    ordering: str | tuple[str, ...]  # Required: field(s) to order by
    template: str
    
    def __init__(self) -> None: ...
    
    def get_ordering(self, request: Request, queryset: QuerySet, view: APIView) -> tuple[str, ...]: ...
    def get_page_size(self, request: Request) -> int: ...
    def encode_cursor(self, cursor: Cursor) -> str: ...
    def decode_cursor(self, request: Request) -> Cursor | None: ...

Configuration:

  • ordering: str | tuple[str, ...] - Required ordering field(s) for cursor positioning
  • cursor_query_param: str - Query parameter name for cursor

Pagination Utilities

Helper Classes

class Cursor(NamedTuple):
    """Cursor position for cursor pagination."""
    
    offset: int
    reverse: bool
    position: Any

class PageLink(NamedTuple):
    """Page link for pagination controls."""
    
    url: str
    number: int
    is_active: bool
    is_break: bool

Utility Functions

def _positive_int(
    integer_string: str, 
    strict: bool = False, 
    cutoff: int | None = None
) -> int:
    """Convert string to positive integer with validation."""
    ...

def _divide_with_ceil(a: int, b: int) -> int:
    """Divide with ceiling for page calculations."""
    ...

def _get_displayed_page_numbers(current: int, final: int) -> list[int | None]:
    """Get page numbers to display in pagination controls."""
    ...

def _get_page_links(
    page_numbers: Sequence[int | None], 
    current: int, 
    url_func: Callable[[int], str]
) -> list[PageLink]:
    """Generate page links for pagination controls."""
    ...

def _reverse_ordering(ordering_tuple: Sequence[str]) -> tuple[str, ...]:
    """Reverse ordering tuple for cursor pagination."""
    ...

Filter Backend Classes

BaseFilterBackend

class BaseFilterBackend:
    """Base class for all filter backends."""
    
    def filter_queryset(
        self,
        request: Request,
        queryset: QuerySet[_MT],
        view: APIView
    ) -> QuerySet[_MT]:
        """
        Filter the queryset based on request parameters.
        
        Args:
            request: Current request object
            queryset: QuerySet to filter
            view: View that owns the queryset
            
        Returns:
            QuerySet[_MT]: Filtered queryset
        """
        ...
    
    def get_schema_fields(self, view: APIView) -> list[Any]:
        """Return schema fields for API documentation."""
        ...
    
    def get_schema_operation_parameters(self, view: APIView) -> list[dict[str, Any]]:
        """Return OpenAPI parameters for this filter."""
        ...

SearchFilter

class SearchFilter(BaseFilterBackend):
    """Filter backend for text search across specified fields."""
    
    # Configuration attributes
    search_param: str  # Default: 'search'
    template: str
    lookup_prefixes: dict[str, str]
    search_title: str
    search_description: str
    
    def get_search_fields(self, view: APIView, request: Request) -> list[str] | None:
        """Get search fields from view configuration."""
        ...
    
    def get_search_terms(self, request: Request) -> list[str]:
        """Extract and parse search terms from request."""
        ...
    
    def construct_search(
        self, 
        field_name: str,
        queryset: QuerySet[_MT] | None = None
    ) -> Q:
        """Construct Q object for search field."""
        ...
    
    def must_call_distinct(
        self, 
        queryset: QuerySet[_MT], 
        search_fields: list[str]
    ) -> bool:
        """Determine if distinct() call is needed."""
        ...

Configuration:

  • search_param: str - Query parameter name for search terms
  • Search fields configured on view via search_fields attribute

OrderingFilter

class OrderingFilter(BaseFilterBackend):
    """Filter backend for result ordering."""
    
    # Configuration attributes
    ordering_param: str  # Default: 'ordering'
    ordering_fields: list[str] | str | None
    ordering_title: str
    ordering_description: str
    template: str
    
    def get_ordering(
        self,
        request: Request,
        queryset: QuerySet[_MT],
        view: APIView
    ) -> Sequence[str] | None:
        """Get ordering from request parameters."""
        ...
    
    def get_default_ordering(self, view: APIView) -> Sequence[str] | None:
        """Get default ordering from view."""
        ...
    
    def get_valid_fields(
        self,
        queryset: QuerySet[_MT],
        view: APIView,
        context: dict[str, Any] | None = None
    ) -> list[tuple[str, str]]:
        """Get valid orderable fields."""
        ...
    
    def remove_invalid_fields(
        self,
        queryset: QuerySet[_MT],
        fields: Sequence[str],
        view: APIView,
        request: Request
    ) -> Sequence[str]:
        """Remove invalid ordering fields."""
        ...

Configuration:

  • ordering_param: str - Query parameter name for ordering
  • ordering_fields: list[str] | str | None - Allowed ordering fields

Usage Examples

Basic Pagination Setup

from rest_framework.pagination import PageNumberPagination
from rest_framework import generics

class CustomPageNumberPagination(PageNumberPagination):
    """Custom page number pagination configuration."""
    
    page_size = 20
    page_size_query_param = 'page_size'
    max_page_size = 100
    last_page_strings = ('last', 'final')

class BookListView(generics.ListAPIView[Book]):
    """List view with pagination."""
    
    queryset = Book.objects.all()
    serializer_class = BookSerializer
    pagination_class = CustomPageNumberPagination
    
    # Response format:
    # {
    #     "count": 150,
    #     "next": "http://api.example.com/books/?page=2", 
    #     "previous": null,
    #     "results": [...]
    # }

Limit/Offset Pagination

class CustomLimitOffsetPagination(LimitOffsetPagination):
    """Custom limit/offset pagination."""
    
    default_limit = 25
    limit_query_param = 'limit'
    offset_query_param = 'offset'
    max_limit = 200

class AuthorViewSet(viewsets.ModelViewSet[Author]):
    """ViewSet with limit/offset pagination."""
    
    queryset = Author.objects.all()
    serializer_class = AuthorSerializer
    pagination_class = CustomLimitOffsetPagination
    
    # Usage: /api/authors/?limit=50&offset=100
    # Response format:
    # {
    #     "count": 500,
    #     "next": "http://api.example.com/authors/?limit=50&offset=150",
    #     "previous": "http://api.example.com/authors/?limit=50&offset=50", 
    #     "results": [...]
    # }

Cursor Pagination

class TimestampCursorPagination(CursorPagination):
    """Cursor pagination ordered by timestamp."""
    
    page_size = 30
    ordering = '-created_at'  # Most recent first
    cursor_query_param = 'cursor'
    page_size_query_param = 'page_size'

class ActivityLogViewSet(viewsets.ReadOnlyModelViewSet[ActivityLog]):
    """ViewSet for activity logs with cursor pagination."""
    
    queryset = ActivityLog.objects.all()
    serializer_class = ActivityLogSerializer
    pagination_class = TimestampCursorPagination
    
    # Usage: /api/activity/?cursor=eyJjcmVhdGVkX2F0IjoiMjAyMy0xMC0xNVQxNDozMDowMFoifQ
    # Response format:
    # {
    #     "next": "http://api.example.com/activity/?cursor=...",
    #     "previous": "http://api.example.com/activity/?cursor=...",
    #     "results": [...]
    # }

Search and Ordering Filters

from rest_framework.filters import SearchFilter, OrderingFilter

class BookSearchViewSet(viewsets.ReadOnlyModelViewSet[Book]):
    """ViewSet with search and ordering capabilities."""
    
    queryset = Book.objects.all()
    serializer_class = BookSerializer
    filter_backends = [SearchFilter, OrderingFilter]
    
    # Search configuration
    search_fields = [
        'title',           # Exact field match
        'description',     # Exact field match  
        '^title',         # Starts-with search
        '=author__name',  # Exact match on related field
        '@title',         # Full-text search (PostgreSQL)
    ]
    
    # Ordering configuration  
    ordering_fields = ['title', 'published_date', 'rating', 'author__name']
    ordering = ['-published_date']  # Default ordering
    
    # Usage examples:
    # /api/books/?search=django&ordering=title
    # /api/books/?search=python tutorial&ordering=-rating,title

Combined Filtering and Pagination

from django_filters.rest_framework import DjangoFilterBackend

class ComprehensiveBookViewSet(viewsets.ReadOnlyModelViewSet[Book]):
    """ViewSet with comprehensive filtering and pagination."""
    
    queryset = Book.objects.select_related('author', 'publisher')
    serializer_class = BookSerializer
    pagination_class = PageNumberPagination
    
    # Multiple filter backends
    filter_backends = [
        DjangoFilterBackend,  # Field-based filtering
        SearchFilter,         # Text search
        OrderingFilter,       # Result ordering
    ]
    
    # Django-filter configuration
    filterset_fields = {
        'genre': ['exact', 'in'],
        'published_date': ['exact', 'gte', 'lte', 'year'],
        'rating': ['exact', 'gte', 'lte'],
        'author__country': ['exact'],
        'is_published': ['exact'],
    }
    
    # Search configuration
    search_fields = ['title', 'description', 'author__name', 'isbn']
    
    # Ordering configuration
    ordering_fields = ['title', 'published_date', 'rating', 'pages']
    ordering = ['-published_date', 'title']
    
    # Usage examples:
    # /api/books/?genre=fiction&rating__gte=4.0&search=python&ordering=-rating
    # /api/books/?published_date__year=2023&author__country=US&page=2
    # /api/books/?genre__in=fiction,mystery&published_date__gte=2020-01-01

Custom Pagination Classes

Custom Page Number Pagination

class CustomPageNumberPagination(PageNumberPagination):
    """Enhanced page number pagination with additional metadata."""
    
    page_size = 20
    page_size_query_param = 'page_size'
    max_page_size = 100
    
    def get_paginated_response(self, data: list[dict[str, Any]]) -> Response:
        """Return custom paginated response format."""
        
        return Response({
            'pagination': {
                'count': self.page.paginator.count,
                'num_pages': self.page.paginator.num_pages,
                'current_page': self.page.number,
                'page_size': self.get_page_size(self.request),
                'has_next': self.page.has_next(),
                'has_previous': self.page.has_previous(),
                'next_page': self.page.next_page_number() if self.page.has_next() else None,
                'previous_page': self.page.previous_page_number() if self.page.has_previous() else None,
            },
            'links': {
                'next': self.get_next_link(),
                'previous': self.get_previous_link(),
                'first': self.get_first_link(),
                'last': self.get_last_link(),
            },
            'results': data
        })

Header-Based Pagination

class HeaderPagination(PageNumberPagination):
    """Pagination that returns metadata in headers instead of response body."""
    
    page_size = 50
    
    def get_paginated_response(self, data: list[dict[str, Any]]) -> Response:
        """Return response with pagination data in headers."""
        
        response = Response(data)
        
        # Add pagination headers
        response['X-Total-Count'] = str(self.page.paginator.count)
        response['X-Page-Count'] = str(self.page.paginator.num_pages)
        response['X-Current-Page'] = str(self.page.number)
        response['X-Page-Size'] = str(self.get_page_size(self.request))
        
        # Add link headers
        links = []
        if self.get_next_link():
            links.append(f'<{self.get_next_link()}>; rel="next"')
        if self.get_previous_link():
            links.append(f'<{self.get_previous_link()}>; rel="prev"')
        if self.get_first_link():
            links.append(f'<{self.get_first_link()}>; rel="first"')
        if self.get_last_link():
            links.append(f'<{self.get_last_link()}>; rel="last"')
        
        if links:
            response['Link'] = ', '.join(links)
        
        return response

Custom Filter Backends

Range Filter Backend

class RangeFilterBackend(BaseFilterBackend):
    """Filter backend for numeric range filtering."""
    
    def filter_queryset(
        self,
        request: Request,
        queryset: QuerySet[_MT],
        view: APIView
    ) -> QuerySet[_MT]:
        """Apply range filters to queryset."""
        
        range_fields = getattr(view, 'range_fields', [])
        
        for field in range_fields:
            min_param = f"{field}__min"
            max_param = f"{field}__max"
            
            min_value = request.query_params.get(min_param)
            max_value = request.query_params.get(max_param)
            
            if min_value is not None:
                try:
                    min_value = float(min_value)
                    queryset = queryset.filter(**{f"{field}__gte": min_value})
                except (ValueError, TypeError):
                    pass
            
            if max_value is not None:
                try:
                    max_value = float(max_value)
                    queryset = queryset.filter(**{f"{field}__lte": max_value})
                except (ValueError, TypeError):
                    pass
        
        return queryset

class BookViewSet(viewsets.ReadOnlyModelViewSet[Book]):
    """ViewSet with range filtering."""
    
    queryset = Book.objects.all()
    serializer_class = BookSerializer
    filter_backends = [RangeFilterBackend]
    range_fields = ['price', 'rating', 'pages']
    
    # Usage: /api/books/?price__min=10.00&price__max=50.00&rating__min=4.0

Geographic Filter Backend

class GeographicFilterBackend(BaseFilterBackend):
    """Filter backend for geographic proximity searches."""
    
    def filter_queryset(
        self,
        request: Request,
        queryset: QuerySet[_MT],
        view: APIView
    ) -> QuerySet[_MT]:
        """Filter by geographic proximity."""
        
        lat = request.query_params.get('lat')
        lon = request.query_params.get('lon')
        radius = request.query_params.get('radius', '10')  # Default 10km
        
        if lat and lon:
            try:
                lat = float(lat)
                lon = float(lon)
                radius = float(radius)
                
                # Use Django's distance filtering (requires PostGIS)
                from django.contrib.gis.measure import Distance
                from django.contrib.gis.geos import Point
                
                point = Point(lon, lat, srid=4326)
                queryset = queryset.filter(
                    location__distance_lte=(point, Distance(km=radius))
                ).annotate(
                    distance=Distance('location', point)
                ).order_by('distance')
                
            except (ValueError, TypeError, ImportError):
                pass  # Skip invalid coordinates or missing PostGIS
        
        return queryset

Performance Optimization

Optimized Pagination

class OptimizedPageNumberPagination(PageNumberPagination):
    """Pagination optimized for large datasets."""
    
    page_size = 25
    max_page_size = 100
    
    def paginate_queryset(
        self,
        queryset: QuerySet[_MT],
        request: Request,
        view: APIView | None = None
    ) -> list[_MT] | None:
        """Optimized pagination with count estimation for large datasets."""
        
        # For very large datasets, estimate count instead of exact count
        if queryset.count() > 100000:  # Threshold for estimation
            # Use EXPLAIN to estimate count
            from django.db import connection
            
            cursor = connection.cursor()
            cursor.execute(f"EXPLAIN (FORMAT JSON) {str(queryset.query)}")
            explain = cursor.fetchone()[0]
            estimated_count = explain[0]['Plan']['Plan Rows']
            
            # Override the paginator's count property
            paginator = self.django_paginator_class(queryset, self.get_page_size(request))
            paginator._count = estimated_count
            self.page = paginator.page(self.get_page_number(request, paginator))
        else:
            # Use normal pagination for smaller datasets
            return super().paginate_queryset(queryset, request, view)
        
        return list(self.page)

Cached Filtering

from django.core.cache import cache

class CachedSearchFilter(SearchFilter):
    """Search filter with result caching."""
    
    def filter_queryset(
        self,
        request: Request,
        queryset: QuerySet[_MT],
        view: APIView
    ) -> QuerySet[_MT]:
        """Filter queryset with caching for expensive searches."""
        
        search_terms = self.get_search_terms(request)
        if not search_terms:
            return queryset
        
        # Create cache key from search terms and queryset
        cache_key = f"search_{hash(tuple(search_terms))}_{queryset.query.__hash__()}"
        cached_ids = cache.get(cache_key)
        
        if cached_ids is not None:
            # Return queryset filtered by cached IDs
            return queryset.filter(id__in=cached_ids)
        
        # Perform search and cache results
        filtered_queryset = super().filter_queryset(request, queryset, view)
        result_ids = list(filtered_queryset.values_list('id', flat=True))
        
        # Cache for 15 minutes
        cache.set(cache_key, result_ids, timeout=900)
        
        return filtered_queryset

This comprehensive pagination and filtering system provides type-safe data management with full mypy support, enabling confident implementation of scalable API endpoints with efficient data access patterns.

Install with Tessl CLI

npx tessl i tessl/pypi-djangorestframework-stubs

docs

authentication-permissions.md

exceptions-status.md

fields-relations.md

index.md

pagination-filtering.md

requests-responses.md

routers-urls.md

serializers.md

views-viewsets.md

tile.json