from django.db import models

class ItemQuerySet(models.QuerySet):
    def public(self):
        return self.filter(is_public=True)

class Item(models.Model):
    is_public = models.BooleanField()
    description = models.CharField()

    objects = ItemQuerySet.as_manager()

대안 1. QuerySet

django-stubs를 적용하지 않았기 때문에, 타입 검사기로 Pylance를 사용한다.

쿼리셋 객체는 기본적으로 Django가 제공하는 QuerySet에 해당하므로 QuerySet을 타입 힌트에 적을 수 있다. 다만, 커스텀 쿼리셋에서 정의한 메서드를 호출하면 타입 에러가 발생한다.

from django.db.models import QuerySet

items: QuerySet = Item.objects.all()
items.count()
items.public()  # Cannot access member "public" for type "BaseManager[Unknown]"
#  Member "public" is unknownPylancereportAttributeAccessIssue Pylance(reportAttributeAccessIssue)

for item in items:
    print(item.description)

커스텀 쿼리셋 클래스를 ORM 결과의 타입으로 지정한다. 커스텀 쿼리셋에 정의한 메서드 호출은 허용되지만, ORM 결과를 지역변수에 할당할 때 타입 에러가 발생한다.

Django QuerySet에는 __contains__() 매직 메서드가 정의되어 있진 않지만, in 연산자가 정상적으로 작동하는 이유는 뭘까?

items: ItemQuerySet = Item.objects.all()  # Expression of type "BaseManager[Item]" cannot be assigned to declared type "ItemQuerySet"
#  "BaseManager[Item]" is incompatible with "ItemQuerySet" Pylance(reportAssignmentType)
items.count()
items.public()

for item in items:
    print(item.description)

대안 2. Sequence

ORM에서 반환한 쿼리셋의 타입을 Sequence로 지정한다. Sequence 원소의 속성과 메서드에 접근할 때 자동완성 기능을 이용할 수 있다.

items: Sequence[Item] = Item.objects.all()  # Expression of type "BaseManager[Item]" cannot be assigned to declared type "Sequence[Item]"
#  "BaseManager[Item]" is incompatible with "Sequence[Item]"PylancereportAssignmentType
items.count()  # Argument missing for parameter "value" Pylance(reportCallIssue)
items.filter(is_public=True)  # "Sequence[Item]" has no attribute "filter" Mypy(attr-defined)
# Cannot access member "filter" for type "Sequence[Item]"
#  Member "filter" is unknown Pylance(reportAttributeAccessIssue)

for item in items:
    print(item.description)

오류

Python docs에 따르면 Sequence는 믹스인 메서드로 __contains__(), count() 등을 가진다. 반면, QuerySet에는 __contains__() 메서드가 정의되어 있지 않을 뿐만 아니라, count()Sequence.count()와 매개변수 형식이 다르다. 따라서 엄밀히 Sequence는 QuerySet의 부분집합이 아니다.

Untitled

참고로, django-stubs에서는 QuerySet 타입을 Sequence가 아닌, Collection, Reversible, Sized를 직접 상속하여 정의한다.

대안 3. django-stubs 활용

https://github.com/typeddjango/django-stubs

실제 실행해보진 않음

from django.db.models import QuerySet

items: QuerySet[Item] = Item.objects.all()
items.count()
items.public()

for item in items:
    print(item.description)