Type stubs for Django.
Note: this project was forked from https://github.com/typeddjango/django-stubs with the goal of removing the
mypyplugin dependency so that
mypycan't crash due to Django config, and that non-
mypytype checkers like
pyrightwill work better with Django.
pip install django-types
You'll need to monkey patch Django's
Manager (not needed for Django 3.1+) and
ForeignKey (not needed for Django 4.1+) 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
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
role_id can be accessed. Also,
will be typed to their right objects.
To be able to access the related manager
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]'
An alternative is using annotations:
from __future__ import annotations # or just be in python 3.11 from typing import TYPE_CHECKING if TYPE_CHECKING: from django.db.models import Manager from user.models import User class Team(models.Model): user_set: Manager[User] class Role(models.Model): users: Manager[User] reveal_type(Team().user_set) # note: Revealed type is 'Manager[User]' reveal_type(Role().users) # note: Revealed type is 'Manager[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
# before class Post(models.Model): ... # after class Post(models.Model): id = models.AutoField(primary_key=True) # OR id: int
user property has a type of
but for most of your views you'll probably want either an authed user or an
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:
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
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: 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:
def activity(request: HttpRequest, team_id: str) -> HttpResponse: ...