django-types
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 that mypy
can't crash due to Django
config, and that
non-mypy
type checkers like
pyright
will work better with
Django.
install
pip install django-types
You can get a TypeError: 'type' object is not subscriptable
when you will try to use QuerySet[MyModel]
, Manager[MyModel]
or some other Django-based Generic types.
This happens because these Django classes do not support __class_getitem__
magic method in runtime.
-
You can go with django_stubs_ext
helper, that patches all the types we use as Generic in django.
Install it:
pip install django-stubs-ext
And then place in your top-level settings:
import django_stubs_ext
django_stubs_ext.monkeypatch()
You can add extra types to patch with django_stubs_ext.monkeypatch(extra_classes=[YourDesiredType])
-
You can use strings instead: 'QuerySet[MyModel]'
and 'Manager[MyModel]'
, this way it will work as a type for type-checking and as a regular str
in runtime.
usage
ForeignKey ids and related names as properties in ORM models
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 foreign 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:
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)
reveal_type(User().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:
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)
reveal_type(Role().users)
An alternative is using annotations:
from __future__ import annotations
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)
reveal_type(Role().users)
id Field
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.
class Post(models.Model):
...
class Post(models.Model):
id = models.AutoField(primary_key=True)
id: int
HttpRequest
's user
property
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
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 function 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]
_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)
raise AuthenticationRequired
return cast(_F, wrapped_view)
Then the following will type error:
@auth.login_required
def activity(request: HttpRequest, team_id: str) -> HttpResponse:
...
related