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

exceptions-status.mddocs/

Exceptions & Status

Django REST Framework provides a comprehensive exception handling system with typed error responses and HTTP status codes. The type stubs ensure type-safe exception handling, error detail formatting, and status code management.

Exception Classes

APIException

class APIException(Exception):
    """Base class for all REST framework exceptions."""
    
    status_code: int
    default_detail: str | dict[str, Any] | list[Any]
    default_code: str
    detail: Any
    
    def __init__(
        self,
        detail: str | dict[str, Any] | list[Any] | None = None,
        code: str | None = None
    ) -> None: ...
    
    def get_codes(self) -> str | dict[str, Any] | list[Any]:
        """
        Return error codes for the exception.
        
        Returns:
            str | dict[str, Any] | list[Any]: Error codes structure
        """
        ...
    
    def get_full_details(self) -> dict[str, Any] | list[dict[str, Any]]:
        """
        Return full error details including codes and messages.
        
        Returns:
            dict[str, Any] | list[dict[str, Any]]: Complete error information
        """
        ...
    
    def __str__(self) -> str: ...

Validation Exceptions

class ValidationError(APIException):
    """Exception raised when serializer validation fails."""
    
    status_code: int  # 400
    default_code: str  # 'invalid'
    
    def __init__(
        self,
        detail: str | dict[str, Any] | list[Any] | None = None,
        code: str | None = None
    ) -> None: ...

class ParseError(APIException):
    """Exception raised when request parsing fails."""
    
    status_code: int  # 400
    default_detail: str  # 'Malformed request.'
    default_code: str  # 'parse_error'

Authentication Exceptions

class AuthenticationFailed(APIException):
    """Exception raised when authentication fails."""
    
    status_code: int  # 401
    default_detail: str  # 'Incorrect authentication credentials.'
    default_code: str  # 'authentication_failed'

class NotAuthenticated(APIException):
    """Exception raised when authentication is required but not provided."""
    
    status_code: int  # 401
    default_detail: str  # 'Authentication credentials were not provided.'
    default_code: str  # 'not_authenticated'

Permission Exceptions

class PermissionDenied(APIException):
    """Exception raised when permission check fails."""
    
    status_code: int  # 403
    default_detail: str  # 'You do not have permission to perform this action.'
    default_code: str  # 'permission_denied'

Resource Exceptions

class NotFound(APIException):
    """Exception raised when a requested resource is not found."""
    
    status_code: int  # 404
    default_detail: str  # 'Not found.'
    default_code: str  # 'not_found'

class MethodNotAllowed(APIException):
    """Exception raised when HTTP method is not allowed."""
    
    status_code: int  # 405
    default_detail: str  # 'Method "{method}" not allowed.'
    default_code: str  # 'method_not_allowed'
    
    def __init__(self, method: str, detail: str | None = None, code: str | None = None) -> None: ...

class NotAcceptable(APIException):
    """Exception raised when requested content type cannot be satisfied."""
    
    status_code: int  # 406
    default_detail: str  # 'Could not satisfy the request Accept header.'
    default_code: str  # 'not_acceptable'

class UnsupportedMediaType(APIException):
    """Exception raised when request media type is not supported."""
    
    status_code: int  # 415
    default_detail: str  # 'Unsupported media type "{media_type}" in request.'
    default_code: str  # 'unsupported_media_type'
    
    def __init__(self, media_type: str, detail: str | None = None, code: str | None = None) -> None: ...

Rate Limiting Exception

class Throttled(APIException):
    """Exception raised when request is throttled."""
    
    status_code: int  # 429
    default_detail: str  # 'Request was throttled.'
    default_code: str  # 'throttled'
    extra_detail_singular: str
    extra_detail_plural: str
    
    def __init__(self, wait: float | None = None, detail: str | None = None, code: str | None = None) -> None: ...

Parameters:

  • wait: float | None - Number of seconds until throttling expires

Error Detail Types

ErrorDetail

class ErrorDetail(str):
    """Enhanced string that includes an error code."""
    
    code: str | None
    
    def __new__(cls, string: str, code: str | None = None) -> ErrorDetail: ...
    def __eq__(self, other: Any) -> bool: ...
    def __ne__(self, other: Any) -> bool: ...
    def __repr__(self) -> str: ...
    def __hash__(self) -> int: ...

Type Aliases

# Complex type definitions for error structures
_Detail = Union[
    str,
    ErrorDetail, 
    dict[str, Any],
    list[Any]
]

_APIExceptionInput = Union[
    str,
    dict[str, Any],
    list[Any],
    ErrorDetail
]

_ErrorCodes = Union[
    str,
    dict[str, Any],
    list[Any]
]

_ErrorFullDetails = Union[
    dict[str, Any],
    list[dict[str, Any]]
]

Exception Handling Functions

Error Detail Processing

def _get_error_details(
    data: _APIExceptionInput, 
    default_code: str | None = None
) -> _Detail:
    """
    Convert exception input to structured error details.
    
    Args:
        data: Raw error data
        default_code: Default error code if none provided
        
    Returns:
        _Detail: Structured error details
    """
    ...

def _get_codes(detail: _Detail) -> _ErrorCodes:
    """
    Extract error codes from error details.
    
    Args:
        detail: Error details structure
        
    Returns:
        _ErrorCodes: Error codes structure
    """
    ...

def _get_full_details(detail: _Detail) -> _ErrorFullDetails:
    """
    Get full error details including codes and messages.
    
    Args:
        detail: Error details structure
        
    Returns:
        _ErrorFullDetails: Complete error information
    """
    ...

Exception Handlers

def server_error(
    request: HttpRequest | Request,
    *args: Any,
    **kwargs: Any
) -> JsonResponse:
    """
    Handle 500 server errors.
    
    Args:
        request: Current request object
        *args: Additional arguments
        **kwargs: Additional keyword arguments
        
    Returns:
        JsonResponse: JSON error response
    """
    ...

def bad_request(
    request: HttpRequest | Request,
    exception: Exception,
    *args: Any,
    **kwargs: Any
) -> JsonResponse:
    """
    Handle 400 bad request errors.
    
    Args:
        request: Current request object
        exception: Exception that caused the error
        *args: Additional arguments
        **kwargs: Additional keyword arguments
        
    Returns:
        JsonResponse: JSON error response
    """
    ...

HTTP Status Codes

Status Code Constants

# Informational responses
HTTP_100_CONTINUE: Literal[100]
HTTP_101_SWITCHING_PROTOCOLS: Literal[101]
HTTP_102_PROCESSING: Literal[102]

# Success responses  
HTTP_200_OK: Literal[200]
HTTP_201_CREATED: Literal[201]
HTTP_202_ACCEPTED: Literal[202]
HTTP_203_NON_AUTHORITATIVE_INFORMATION: Literal[203]
HTTP_204_NO_CONTENT: Literal[204]
HTTP_205_RESET_CONTENT: Literal[205]
HTTP_206_PARTIAL_CONTENT: Literal[206]
HTTP_207_MULTI_STATUS: Literal[207]
HTTP_208_ALREADY_REPORTED: Literal[208]
HTTP_226_IM_USED: Literal[226]

# Redirection responses
HTTP_300_MULTIPLE_CHOICES: Literal[300]
HTTP_301_MOVED_PERMANENTLY: Literal[301] 
HTTP_302_FOUND: Literal[302]
HTTP_303_SEE_OTHER: Literal[303]
HTTP_304_NOT_MODIFIED: Literal[304]
HTTP_305_USE_PROXY: Literal[305]
HTTP_307_TEMPORARY_REDIRECT: Literal[307]
HTTP_308_PERMANENT_REDIRECT: Literal[308]

# Client error responses
HTTP_400_BAD_REQUEST: Literal[400]
HTTP_401_UNAUTHORIZED: Literal[401]
HTTP_402_PAYMENT_REQUIRED: Literal[402]
HTTP_403_FORBIDDEN: Literal[403]
HTTP_404_NOT_FOUND: Literal[404]
HTTP_405_METHOD_NOT_ALLOWED: Literal[405]
HTTP_406_NOT_ACCEPTABLE: Literal[406]
HTTP_407_PROXY_AUTHENTICATION_REQUIRED: Literal[407]
HTTP_408_REQUEST_TIMEOUT: Literal[408]
HTTP_409_CONFLICT: Literal[409]
HTTP_410_GONE: Literal[410]
HTTP_411_LENGTH_REQUIRED: Literal[411]
HTTP_412_PRECONDITION_FAILED: Literal[412]
HTTP_413_REQUEST_ENTITY_TOO_LARGE: Literal[413]
HTTP_414_REQUEST_URI_TOO_LONG: Literal[414]
HTTP_415_UNSUPPORTED_MEDIA_TYPE: Literal[415]
HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE: Literal[416]
HTTP_417_EXPECTATION_FAILED: Literal[417]
HTTP_418_IM_A_TEAPOT: Literal[418]
HTTP_422_UNPROCESSABLE_ENTITY: Literal[422]
HTTP_423_LOCKED: Literal[423]
HTTP_424_FAILED_DEPENDENCY: Literal[424]
HTTP_426_UPGRADE_REQUIRED: Literal[426]
HTTP_428_PRECONDITION_REQUIRED: Literal[428]
HTTP_429_TOO_MANY_REQUESTS: Literal[429]
HTTP_431_REQUEST_HEADER_FIELDS_TOO_LARGE: Literal[431]
HTTP_451_UNAVAILABLE_FOR_LEGAL_REASONS: Literal[451]

# Server error responses
HTTP_500_INTERNAL_SERVER_ERROR: Literal[500]
HTTP_501_NOT_IMPLEMENTED: Literal[501]
HTTP_502_BAD_GATEWAY: Literal[502]
HTTP_503_SERVICE_UNAVAILABLE: Literal[503]
HTTP_504_GATEWAY_TIMEOUT: Literal[504]
HTTP_505_HTTP_VERSION_NOT_SUPPORTED: Literal[505]
HTTP_507_INSUFFICIENT_STORAGE: Literal[507]
HTTP_508_LOOP_DETECTED: Literal[508]
HTTP_510_NOT_EXTENDED: Literal[510]
HTTP_511_NETWORK_AUTHENTICATION_REQUIRED: Literal[511]

Status Code Utility Functions

def is_informational(code: int) -> bool:
    """Check if status code is informational (1xx)."""
    ...

def is_success(code: int) -> bool:
    """Check if status code indicates success (2xx)."""
    ...

def is_redirect(code: int) -> bool:
    """Check if status code indicates redirection (3xx)."""
    ...

def is_client_error(code: int) -> bool:
    """Check if status code indicates client error (4xx)."""
    ...

def is_server_error(code: int) -> bool:
    """Check if status code indicates server error (5xx)."""
    ...

Exception Usage Examples

Basic Exception Handling

from rest_framework import exceptions, status
from rest_framework.views import APIView
from rest_framework.response import Response

class BookDetailView(APIView):
    """Demonstrate basic exception handling."""
    
    def get(self, request: Request, pk: int) -> Response:
        """Retrieve book with proper exception handling."""
        
        try:
            book = Book.objects.get(pk=pk)
        except Book.DoesNotExist:
            raise exceptions.NotFound("Book not found")
        
        # Check permissions
        if not self.has_permission(request, book):
            raise exceptions.PermissionDenied("Access denied to this book")
        
        serializer = BookSerializer(book)
        return Response(serializer.data)
    
    def put(self, request: Request, pk: int) -> Response:
        """Update book with validation error handling."""
        
        try:
            book = Book.objects.get(pk=pk)
        except Book.DoesNotExist:
            raise exceptions.NotFound("Book not found")
        
        serializer = BookSerializer(book, data=request.data)
        if not serializer.is_valid():
            raise exceptions.ValidationError(serializer.errors)
        
        serializer.save()
        return Response(serializer.data)
    
    def has_permission(self, request: Request, book: Book) -> bool:
        """Check if user has permission to access book."""
        return book.is_public or request.user == book.owner

Custom Exception Classes

class BookNotAvailableError(exceptions.APIException):
    """Custom exception for unavailable books."""
    
    status_code = status.HTTP_409_CONFLICT
    default_detail = 'Book is currently not available'
    default_code = 'book_not_available'

class QuotaExceededError(exceptions.APIException):
    """Custom exception for quota limits."""
    
    status_code = status.HTTP_429_TOO_MANY_REQUESTS
    default_detail = 'Download quota exceeded'
    default_code = 'quota_exceeded'
    
    def __init__(self, limit: int, reset_time: datetime, detail: str | None = None):
        if detail is None:
            detail = f'Quota limit of {limit} exceeded. Resets at {reset_time.isoformat()}'
        super().__init__(detail)
        self.limit = limit
        self.reset_time = reset_time

class InvalidFileFormatError(exceptions.APIException):
    """Custom exception for file format validation."""
    
    status_code = status.HTTP_415_UNSUPPORTED_MEDIA_TYPE
    default_detail = 'Invalid file format'
    default_code = 'invalid_file_format'
    
    def __init__(self, allowed_formats: list[str], received_format: str):
        detail = f'Invalid format "{received_format}". Allowed formats: {", ".join(allowed_formats)}'
        super().__init__(detail)
        self.allowed_formats = allowed_formats
        self.received_format = received_format

Exception Handling in Views

class BookDownloadView(APIView):
    """View demonstrating custom exception usage."""
    
    def post(self, request: Request, pk: int) -> Response:
        """Download book with comprehensive error handling."""
        
        # Get book
        try:
            book = Book.objects.get(pk=pk)
        except Book.DoesNotExist:
            raise exceptions.NotFound({
                'error': 'Book not found',
                'code': 'BOOK_NOT_FOUND',
                'book_id': pk
            })
        
        # Check availability
        if not book.is_available:
            raise BookNotAvailableError()
        
        # Check user quota
        user_downloads = request.user.downloads.filter(
            created_at__date=timezone.now().date()
        ).count()
        
        daily_limit = getattr(request.user, 'daily_download_limit', 10)
        if user_downloads >= daily_limit:
            tomorrow = timezone.now().replace(hour=0, minute=0, second=0, microsecond=0) + timedelta(days=1)
            raise QuotaExceededError(daily_limit, tomorrow)
        
        # Check file format
        requested_format = request.data.get('format', 'pdf')
        allowed_formats = ['pdf', 'epub', 'mobi']
        
        if requested_format not in allowed_formats:
            raise InvalidFileFormatError(allowed_formats, requested_format)
        
        # Process download
        download = Download.objects.create(
            user=request.user,
            book=book,
            format=requested_format
        )
        
        return Response({
            'download_id': download.id,
            'download_url': download.get_download_url(),
            'expires_at': (timezone.now() + timedelta(hours=24)).isoformat()
        }, status=status.HTTP_201_CREATED)

Validation Error Details

from rest_framework import serializers

class BookSerializer(serializers.ModelSerializer):
    """Serializer with detailed validation errors."""
    
    class Meta:
        model = Book
        fields = ['title', 'author', 'isbn', 'published_date', 'pages']
    
    def validate_isbn(self, value: str) -> str:
        """Validate ISBN with detailed error."""
        if len(value) not in [10, 13]:
            raise serializers.ValidationError(
                detail='ISBN must be 10 or 13 characters long',
                code='invalid_length'
            )
        
        # Check if ISBN already exists
        if Book.objects.filter(isbn=value).exists():
            raise serializers.ValidationError(
                detail='Book with this ISBN already exists',
                code='duplicate_isbn'
            )
        
        return value
    
    def validate_pages(self, value: int) -> int:
        """Validate page count."""
        if value <= 0:
            raise serializers.ValidationError(
                detail='Page count must be positive',
                code='invalid_page_count'
            )
        
        if value > 10000:
            raise serializers.ValidationError(
                detail='Page count exceeds maximum limit of 10,000',
                code='page_count_too_high'
            )
        
        return value
    
    def validate(self, data: dict[str, Any]) -> dict[str, Any]:
        """Cross-field validation."""
        published_date = data.get('published_date')
        
        if published_date and published_date > timezone.now().date():
            raise serializers.ValidationError({
                'published_date': ErrorDetail(
                    'Published date cannot be in the future',
                    code='future_date'
                )
            })
        
        return data

Custom Exception Handler

Global Exception Handler

from rest_framework.views import exception_handler as drf_exception_handler
from rest_framework.response import Response
import logging

logger = logging.getLogger(__name__)

def custom_exception_handler(exc: Exception, context: dict[str, Any]) -> Response | None:
    """Custom exception handler with enhanced error formatting."""
    
    # Get the standard error response
    response = drf_exception_handler(exc, context)
    
    if response is not None:
        # Log the error
        request = context.get('request')
        view = context.get('view')
        
        logger.error(
            f"API Error: {exc.__class__.__name__} - {str(exc)} - "
            f"View: {view.__class__.__name__ if view else 'Unknown'} - "
            f"User: {getattr(request, 'user', 'Anonymous') if request else 'Unknown'} - "
            f"Path: {getattr(request, 'path', 'Unknown') if request else 'Unknown'}"
        )
        
        # Customize error response format
        custom_response_data = {
            'success': False,
            'error': {
                'type': exc.__class__.__name__,
                'message': str(exc),
                'status_code': response.status_code,
                'timestamp': timezone.now().isoformat(),
            }
        }
        
        # Add detailed error information for validation errors
        if isinstance(exc, ValidationError) and hasattr(exc, 'detail'):
            custom_response_data['error']['details'] = exc.detail
            
            # Add field-specific error codes
            if hasattr(exc, 'get_codes'):
                custom_response_data['error']['codes'] = exc.get_codes()
        
        # Add request context in debug mode
        if settings.DEBUG and request:
            custom_response_data['debug'] = {
                'request_method': request.method,
                'request_path': request.path,
                'user': str(request.user) if hasattr(request, 'user') else 'Unknown',
                'view_name': view.__class__.__name__ if view else 'Unknown',
            }
        
        response.data = custom_response_data
    
    return response

# Configure in settings.py:
# REST_FRAMEWORK = {
#     'EXCEPTION_HANDLER': 'myapp.exceptions.custom_exception_handler'
# }

Exception Response Middleware

class ExceptionResponseMiddleware:
    """Middleware for consistent exception response formatting."""
    
    def __init__(self, get_response: Callable) -> None:
        self.get_response = get_response
    
    def __call__(self, request: HttpRequest) -> HttpResponse:
        response = self.get_response(request)
        return response
    
    def process_exception(self, request: HttpRequest, exception: Exception) -> JsonResponse | None:
        """Handle exceptions not caught by DRF."""
        
        if not request.path.startswith('/api/'):
            return None  # Let Django handle non-API requests
        
        # Handle specific exception types
        if isinstance(exception, ValueError):
            return JsonResponse({
                'success': False,
                'error': {
                    'type': 'ValueError',
                    'message': 'Invalid input data',
                    'status_code': 400,
                    'timestamp': timezone.now().isoformat()
                }
            }, status=400)
        
        elif isinstance(exception, PermissionError):
            return JsonResponse({
                'success': False,
                'error': {
                    'type': 'PermissionError',
                    'message': 'Permission denied',
                    'status_code': 403,
                    'timestamp': timezone.now().isoformat()
                }
            }, status=403)
        
        # Handle unexpected exceptions
        logger.exception(f"Unhandled exception in API: {exception}")
        
        if not settings.DEBUG:
            return JsonResponse({
                'success': False,
                'error': {
                    'type': 'InternalServerError',
                    'message': 'An unexpected error occurred',
                    'status_code': 500,
                    'timestamp': timezone.now().isoformat()
                }
            }, status=500)
        
        return None  # Let Django's debug handler take over in debug mode

Testing Exception Handling

Exception Testing

from rest_framework.test import APITestCase
from rest_framework import status

class ExceptionHandlingTests(APITestCase):
    """Test exception handling behavior."""
    
    def test_not_found_exception(self) -> None:
        """Test 404 exception handling."""
        response = self.client.get('/api/books/999/')
        
        self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
        self.assertIn('error', response.data)
        self.assertEqual(response.data['error']['type'], 'NotFound')
    
    def test_validation_exception(self) -> None:
        """Test validation error handling."""
        invalid_data = {
            'title': '',  # Empty title should fail validation
            'isbn': '123',  # Invalid ISBN length
        }
        
        response = self.client.post('/api/books/', invalid_data)
        
        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
        self.assertIn('details', response.data['error'])
        self.assertIn('title', response.data['error']['details'])
        self.assertIn('isbn', response.data['error']['details'])
    
    def test_permission_exception(self) -> None:
        """Test permission denied handling."""
        # Create a private book owned by another user
        other_user = User.objects.create_user('other', 'other@example.com', 'password')
        book = Book.objects.create(title='Private Book', owner=other_user, is_private=True)
        
        response = self.client.get(f'/api/books/{book.id}/')
        
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
        self.assertEqual(response.data['error']['type'], 'PermissionDenied')
    
    def test_custom_exception(self) -> None:
        """Test custom exception handling."""
        # Create scenario that triggers custom exception
        book = Book.objects.create(title='Test Book', is_available=False)
        
        response = self.client.post(f'/api/books/{book.id}/download/', {'format': 'pdf'})
        
        self.assertEqual(response.status_code, status.HTTP_409_CONFLICT)
        self.assertEqual(response.data['error']['type'], 'BookNotAvailableError')

This comprehensive exception and status system provides type-safe error handling with full mypy support, enabling confident implementation of robust error management and HTTP status code handling in Django REST Framework applications.

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