Type stubs for Django.
Note: this project was forked from https://github.com/typeddjango/django-stubs with the goal of removing the
mypy
plugin dependency so thatmypy
can't crash due to Django config, and that non-mypy
type checkers likepyright
will work better with Django.
pip install django-types
You'll need to monkey patch Django's QuerySet
, Manager
(not needed for Django 3.1+) and
ForeignKey
classes so we can index into them with a generic argument. Add this to your
settings.py:
# in settings.py
from django.db.models import ForeignKey
from django.db.models.manager import BaseManager
from django.db.models.query import QuerySet
# NOTE: there are probably other items you'll need to monkey patch depending on
# your version.
for cls in [QuerySet, BaseManager, ForeignKey]:
cls.__class_getitem__ = classmethod(lambda cls, *args, **kwargs: cls) # type: ignore [attr-defined]
When defining a Django ORM model with a foreign key, like so:
class User(models.Model):
team = models.ForeignKey(
"Team",
null=True,
on_delete=models.SET_NULL,
)
role = models.ForeignKey(
"Role",
null=True,
on_delete=models.SET_NULL,
related_name="users",
)
two properties are created, team
as expected, and team_id
. Also, a related
manager called user_set
is created on Team
for the reverse access.
In order to properly add typing to the foreing key itself and also for the created ids you can do something like this:
from typing import TYPE_CHECKING
from someapp.models import Team
if TYPE_CHECKING:
# In this example Role cannot be imported due to circular import issues,
# but doing so inside TYPE_CHECKING will make sure that the typing bellow
# knows what "Role" means
from anotherapp.models import Role
class User(models.Model):
team_id: Optional[int]
team = models.ForeignKey(
Team,
null=True,
on_delete=models.SET_NULL,
)
role_id: int
role = models.ForeignKey["Role"](
"Role",
null=False,
on_delete=models.SET_NULL,
related_name="users",
)
reveal_type(User().team)
# note: Revealed type is 'Optional[Team]'
reveal_type(User().role)
# note: Revealed type is 'Role'
This will make sure that team_id
and role_id
can be accessed. Also, team
and role
will be typed to their right objects.
To be able to access the related manager Team
and Role
you could do:
from typing import TYPE_CHECKING
if TYPE_CHECKING:
# This doesn't really exists on django so it always need to be imported this way
from django.db.models.manager import RelatedManager
from user.models import User
class Team(models.Model):
if TYPE_CHECKING:
user_set = RelatedManager["User"]()
class Role(models.Model):
if TYPE_CHECKING:
users = RelatedManager["User"]()
reveal_type(Team().user_set)
# note: Revealed type is 'RelatedManager[User]'
reveal_type(Role().users)
# note: Revealed type is 'RelatedManager[User]'
By default Django will create an AutoField
for you if one doesn't exist.
For type checkers to know about the id
field you'll need to declare the
field explicitly.
# before
class Post(models.Model):
...
# after
class Post(models.Model):
id = models.AutoField(primary_key=True)
# OR
id: int
The HttpRequest
's user
property has a type of Union[AbstractBaseUser, AnonymousUser]
,
but for most of your views you'll probably want either an authed user or an
AnonymousUser
.
So we can define a subclass for each case:
class AuthedHttpRequest(HttpRequest):
user: User # type: ignore [assignment]
And then you can use it in your views:
@auth.login_required
def activity(request: AuthedHttpRequest, team_id: str) -> HttpResponse:
...
You can also get more strict with your login_required
decorator so that the
first argument of the fuction it is decorating is AuthedHttpRequest
:
from typing import Any, Union, TypeVar, cast
from django.http import HttpRequest, HttpResponse
from typing_extensions import Protocol
from functools import wraps
class RequestHandler1(Protocol):
def __call__(self, request: AuthedHttpRequest) -> HttpResponse:
...
class RequestHandler2(Protocol):
def __call__(self, request: AuthedHttpRequest, __arg1: Any) -> HttpResponse:
...
RequestHandler = Union[RequestHandler1, RequestHandler2]
# Verbose bound arg due to limitations of Python typing.
# see: https://github.com/python/mypy/issues/5876
_F = TypeVar("_F", bound=RequestHandler)
def login_required(view_func: _F) -> _F:
@wraps(view_func)
def wrapped_view(
request: AuthedHttpRequest, *args: object, **kwargs: object
) -> HttpResponse:
if request.user.is_authenticated:
return view_func(request, *args, **kwargs) # type: ignore [call-arg]
raise AuthenticationRequired
return cast(_F, wrapped_view)
Then the following will type error:
@auth.login_required
def activity(request: HttpRequest, team_id: str) -> HttpResponse:
...