Skip to content

InheritanceQuerySetMixin methods should return Self instead of InheritanceQuerySet[ModelT] #651

@lee3jjang

Description

@lee3jjang

Problem

When subclassing InheritanceQuerySetMixin to create a custom QuerySet, methods that return Self fail mypy type checking because annotate(), _chain(), and _clone() are typed to return InheritanceQuerySet[ModelT] instead of Self.

from typing import Self
from django.db.models import QuerySet
from model_utils.managers import InheritanceQuerySetMixin

class MyQuerySet(InheritanceQuerySetMixin[MyModel], QuerySet[MyModel]):
    def my_filter(self) -> Self:
        return self.annotate(foo=Value(1))  # mypy error here

mypy error:

error: Incompatible return value type (got "InheritanceQuerySet[MyModel]", expected "Self")  [return-value]

This happens because annotate() in InheritanceQuerySetMixin returns InheritanceQuerySet[ModelT]:

def annotate(self, *args: Any, **kwargs: Any) -> InheritanceQuerySet[ModelT]:
    ...

The same issue affects _chain() and _clone().

Environment

  • Django Model Utils version: 5.0.0
  • Django version: 5.2.9
  • Python version: 3.11

Code example / Failing test

# test_inheritance_manager.py

from typing import Self, Generic
from typing_extensions import TypeVar
from django.db import models
from django.db.models import Value
from django.db.models.query import QuerySet
from model_utils.managers import InheritanceQuerySetMixin, InheritanceManager

T_co = TypeVar("T_co", bound=models.Model, covariant=True)

class CustomQuerySet(InheritanceQuerySetMixin[T_co], QuerySet[T_co], Generic[T_co]):
    def custom_annotate(self) -> Self:
        # This should type-check correctly but currently fails mypy
        return self.annotate(custom_field=Value(1))

# Running mypy on this file produces:
# error: Incompatible return value type (got "InheritanceQuerySet[T_co]", expected "Self")

Proposed Solution

Change return types from InheritanceQuerySet[ModelT] to Self:

# managers.py
import sys
if sys.version_info >= (3, 11):
    from typing import Self
else:
    from typing_extensions import Self

class InheritanceQuerySetMixin(Generic[ModelT]):
    ...

    def annotate(self, *args: Any, **kwargs: Any) -> Self:
        ...

    def _chain(self, **kwargs: object) -> Self:
        ...

    def _clone(self) -> Self:
        ...

This requires adding typing_extensions as a dependency for Python 3.10 support.

Current Workaround

Users must use a two-class pattern:

# Base without mixin - type checks against QuerySet.annotate() -> Self
class MyBaseQuerySet(QuerySet[T_co], Generic[T_co]):
    def my_filter(self) -> Self:
        return self.annotate(...)  # Works!

# Final class adds mixin for select_subclasses() support
class MyQuerySet(InheritanceQuerySetMixin[T_co], MyBaseQuerySet[T_co]):
    pass

I'm happy to submit a PR with tests if this approach is acceptable.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions