- Published on
Type-hinting Django Manager's Model
문제
Django에서 user 정보에 age field를 추가할 일이 있었다.
from typing import override
from django.contrib.auth.models import UserManager, User
from django.db.models import IntegerField
class CustomUserManager(UserManager):
@override
def create_user(
self,
username: str,
email: str | None = None,
password: str | None = None,
**extra_fields: object,
):
age = extra_fields["age"]
if not isinstance(age, int):
raise ValueError("age should be integer")
model = self.model(username=username, email=email, age=age)
model.set_password(password)
model.save()
return model
class CustomUser(User):
age = IntegerField()
objects = CustomUserManager()
이렇게 코드를 작성하면 한 가지 문제가 있다. 바로 CustomUserManager.model의 타입이 Any라는 것이다.
어떻게 하면 올바른 타입으로 추론되도록 할 수 있을까?
Stub
Django는 FastAPI와는 달리 type hint를 프레임워크 차원에서 지원하지 않는다. 대신 stub 파일을 만들면 type hint를 지원하는 것처럼 보이게 할 수 있다1. 다행히 다른 사람들이 작성한 Django stub 파일이 있기 때문에 그걸 쓰면 된다.
# mypy를 쓰는 경우
uv add "django-stubs[compatible-mypy]"
# pyright을 쓰는 경우
uv add django-types
나는 pyright을 사용하고 있기 때문에 django-types를 설치했다.
참고
만약 language server로 pylance를 사용하고 있다면 django-types를 설치할 필요 없다. pylance는 django-types를 내장하고 있기 때문이다2.
pylance 설치 폴더에서 django-types를 볼 수 있다.
예) %USERPROFILE%\.vscode\extensions\ms-python.vscode-pylance-2025.7.1\dist\bundled\stubs\django-stubs
Type Argument
django-types를 설치하면 UserManager에 type argument를 줄 수 있다.
-class CustomUserManager(UserManager):
+class CustomUserManager(UserManager["CustomUser"]):
이렇게 하면 model의 타입이 CustomUser로 추론된다.
How It Works?
django-types 코드를 보면 stub 파일에 UserManager가 이렇게 정의되어 있다.
class UserManager(BaseUserManager[_T]):
...
상속 계층을 따라 올라가면 UserManager -> BaseUserManager -> Manager -> BaseManager -> QuerySet -> _BaseQuerySet으로 가게 되는데, _BaseQuerySet을 보면 model의 타입이 이렇게 정의되어 있다.
_T = TypeVar("_T", bound=models.Model)
class _BaseQuerySet(Generic[_T], Sized):
model: type[_T]
이 코드를 통해서 _BaseQuerySet은 type argument _T를 받고, model이라는 attribute의 타입을 _T의 subclass4로 지정한다는 걸 알 수 있다.
즉, class CustomUserManager(UserManager["CustomUser"])에서 _T로 CustomUser를 준 것이고, model: type[_T]에 따라 model이 CustomUser로 추론된 것이다.
References
Footnotes
stub 파일은 TypeScript의 d.ts 파일과 비슷하다. 기존 module 코드를 수정하지 않고 유저에게 정적 타입을 제공할 수 있기 때문이다. ↩
https://github.com/microsoft/pylance-release/issues/4597#issuecomment-1640577809 ↩
CustomUser를 참조하는 시점엔 아직 CustomUser가 정의되지 않았기에 바로 참조(
[CustomUser]) 할 수 없고 string literal로 참조(["CustomUser"]) 해야 한다. 이를 forward reference라 한다. https://peps.python.org/pep-0484/#forward-references ↩type[_T]는 _T의 subclass를 나타낸다. https://typing.python.org/en/latest/spec/special-types.html#type ↩