PEP 484 type stubs for Django REST Framework enabling static type checking with comprehensive type definitions for all major DRF components
—
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.
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."""
...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 pagepage_query_param: str - Query parameter name for page numberpage_size_query_param: str | None - Query parameter for custom page sizemax_page_size: int | None - Maximum allowed page sizeclass 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 pagelimit_query_param: str - Query parameter name for limitoffset_query_param: str - Query parameter name for offsetmax_limit: int | None - Maximum allowed limitclass 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 positioningcursor_query_param: str - Query parameter name for cursorclass 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: booldef _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."""
...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."""
...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 termssearch_fields attributeclass 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 orderingordering_fields: list[str] | str | None - Allowed ordering fieldsfrom 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": [...]
# }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": [...]
# }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": [...]
# }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,titlefrom 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-01class 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
})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 responseclass 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.0class 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 querysetclass 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)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_querysetThis 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