This package contains type stubs and a custom mypy plugin to provide more precise static types and type inference for Django framework. Django uses some Python "magic" that makes having precise types for some code patterns problematic. This is why we need this project. The final goal is to be able to get precise types for most common patterns.
pip install django-stubs[compatible-mypy]
To make mypy aware of the plugin, you need to add
[mypy]
plugins =
mypy_django_plugin.main
[mypy.plugins.django-stubs]
django_settings_module = "myproject.settings"
in your mypy.ini
or setup.cfg
file.
pyproject.toml configurations are also supported:
[tool.mypy]
plugins = ["mypy_django_plugin.main"]
[tool.django-stubs]
django_settings_module = "myproject.settings"
Two things happening here:
- We need to explicitly list our plugin to be loaded by
mypy
- Our plugin also requires
django
settings module (what you put intoDJANGO_SETTINGS_MODULE
variable) to be specified
This fully working typed boilerplate can serve you as an example.
We rely on different django
and mypy
versions:
django-stubs | Mypy version | Django version | Django partial support | Python version |
---|---|---|---|---|
4.2.1 | 1.3.x | 4.2 | 4.1, 3.2 | 3.8 - 3.11 |
4.2.0 | 1.2.x | 4.2 | 4.1, 4.0, 3.2 | 3.7 - 3.11 |
1.16.0 | 1.1.x | 4.1 | 4.0, 3.2 | 3.7 - 3.11 |
1.15.0 | 1.0.x | 4.1 | 4.0, 3.2 | 3.7 - 3.11 |
1.14.0 | 0.990+ | 4.1 | 4.0, 3.2 | 3.7 - 3.11 |
By inheriting from the TypedModelMeta
class, you can ensure you're using correct types for
attributes:
from django.db import models
from django_stubs_ext.db.models import TypedModelMeta
class MyModel(models.Model):
example = models.CharField(max_length=100)
class Meta(TypedModelMeta):
ordering = ["example"]
constraints = [
models.UniqueConstraint(fields=["example"], name="unique_example"),
]
django_stubs_ext.db.router.TypedDatabaseRouter
can be used as base when implementing custom database routers.
No, it is not. We are independent from Django at the moment. There's a proposal to merge our project into the Django itself. You can show your support by liking the PR.
Yes, it is! This project does not affect your runtime at all.
It only affects mypy
type checking process.
But, it does not make any sense to use this project without mypy
.
The current implementation uses Django's runtime to extract information about models, so it might crash if your installed apps or models.py
are broken.
In other words, if your manage.py runserver
crashes, mypy will crash too.
You can also run mypy
with --tb
option to get extra information about the error.
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 our
django_stubs_ext
helper, that patches all the types we use as Generic in django.Install it:
pip install django-stubs-ext # as a production dependency
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 formypy
and as a regularstr
in runtime.
Django's built in HttpRequest
has the attribute user
that resolves to the type
Union[User, AnonymousUser]
where User
is the user model specified by the AUTH_USER_MODEL
setting.
If you want a HttpRequest
that you can type-annotate with where you know that the user is authenticated you can subclass the normal HttpRequest
class like so:
from django.http import HttpRequest
from my_user_app.models import MyUser
class AuthenticatedHttpRequest(HttpRequest):
user: MyUser
And then use AuthenticatedHttpRequest
instead of the standard HttpRequest
for when you know that the user is authenticated. For example in views using the @login_required
decorator.
If you are using MyQuerySet.as_manager()
:
Example:
from django.db import models
class MyModelQuerySet(models.QuerySet):
pass
class MyModel(models.Model):
bar = models.IntegerField()
objects = MyModelQuerySet.as_manager()
def use_my_model() -> int:
foo = MyModel.objects.get(id=1) # Should now be `MyModel`
return foo.xyz # Gives an error
Or if you're using Manager.from_queryset
:
Example:
from django.db import models
class MyModelQuerySet(models.QuerySet):
pass
MyModelManager = models.Manager.from_queryset(MyModelQuerySet)
class MyModel(models.Model):
bar = models.IntegerField()
objects = MyModelManager()
def use_my_model() -> int:
foo = MyModel.objects.get(id=1) # Should now be `MyModel`
return foo.xyz # Gives an error
If you declare your custom managers without generics and override built-in methods you might see an error message about incompatible error messages, something like this:
from django.db import models
class MyManager(model.Manager):
def create(self, **kwargs) -> "MyModel":
pass
will cause this error message:
error: Return type "MyModel" of "create" incompatible with return type "_T" in supertype "BaseManager"
This is happening because the Manager
class is generic, but without
specifying generics the built-in manager methods are expected to return the
generic type of the base manager, which is any model. To fix this issue you
should declare your manager with your model as the type variable:
class MyManager(models.Manager["MyModel"]):
...
Django-stubs provides a special type, django_stubs_ext.WithAnnotations[Model]
, which indicates that the Model
has
been annotated, meaning it allows getting/setting extra attributes on the model instance.
Optionally, you can provide a TypedDict
of these attributes,
e.g. WithAnnotations[MyModel, MyTypedDict]
, to specify which annotated attributes are present.
Currently, the mypy plugin can recognize that specific names were passed to QuerySet.annotate
and
include them in the type, but does not record the types of these attributes.
The knowledge of the specific annotated fields is not yet used in creating more specific types for QuerySet
's
values
, values_list
, or filter
methods, however knowledge that the model was annotated is used to create a
broader type result type for values
/values_list
, and to allow filter
ing on any field.
from typing import TypedDict
from django_stubs_ext import WithAnnotations
from django.db import models
from django.db.models.expressions import Value
class MyModel(models.Model):
username = models.CharField(max_length=100)
def func(m: WithAnnotations[MyModel]) -> str:
return m.asdf # OK, since the model is annotated as allowing any attribute
func(MyModel.objects.annotate(foo=Value("")).get(id=1)) # OK
func(
MyModel.objects.get(id=1)
) # Error, since this model will not allow access to any attribute
class MyTypedDict(TypedDict):
foo: str
def func2(m: WithAnnotations[MyModel, MyTypedDict]) -> str:
print(m.bar) # Error, since field "bar" is not in MyModel or MyTypedDict.
return m.foo # OK, since we said field "foo" was allowed
func(MyModel.objects.annotate(foo=Value("")).get(id=1)) # OK
func(MyModel.objects.annotate(bar=Value("")).get(id=1)) # Error
A limitation of making QuerySet
generic is that you can not use
it for isinstance
checks.
from django.db.models.query import QuerySet
def foo(obj: object) -> None:
if isinstance(obj, QuerySet): # Error: Parameterized generics cannot be used with class or instance checks
...
To get around with this issue without making QuerySet
non-generic,
Django-stubs provides django_stubs_ext.QuerySetAny
, a non-generic
variant of QuerySet
suitable for runtime type checking:
from django_stubs_ext import QuerySetAny
def foo(obj: object) -> None:
if isinstance(obj, QuerySetAny): # OK
...
The lazy translation functions of Django (such as gettext_lazy
) return a Promise
instead of str
. These two types cannot be used interchangeably. The return type of these functions was therefore changed to reflect that.
If you encounter this error in your own code, you can either cast the Promise
to str
(causing the translation to be evaluated), or use the StrPromise
or StrOrPromise
types from django-stubs-ext
in type hints. Which solution to choose depends depends on the particular case. See working with lazy translation objects in the Django documentation for more information.
If this is reported on Django code, please report an issue or open a pull request to fix the type hints.
awesome-python-typing
- Awesome list of all typing-related things in Python.djangorestframework-stubs
- Stubs for Django REST Framework.pytest-mypy-plugins
-pytest
plugin that we use for testingmypy
stubs and plugins.wemake-django-template
- Create new typed Django projects in seconds.
We have Gitter here: https://gitter.im/mypy-django/Lobby If you think you have more generic typing issue, please refer to https://github.com/python/mypy and their Gitter.
This project is open source and community driven. As such we encourage contributions big and small. You can contribute by doing any of the following:
- Contribute code (e.g. improve stubs, add plugin capabilities, write tests etc) - to do so please follow the contribution guide.
- Assist in code reviews and discussions in issues.
- Identify bugs and issues and report these
- Ask and answer questions on StackOverflow
You can always also reach out in gitter to discuss your contributions!