--- title: Setup Mypy for Django projects date: 2026-09-16 10:47:54.653675 UTC --- After my last post "[How to install a LSP server that supports Django](https://quan.hoabinh.vn/post/2026/9/how-to-install-a-lsp-server-that-supports-django)" was shared, I got feedback that some fellows tried to install the LSP, but didn't get the autocomplete working as shown in my screenshot. So I continue with this article. ![django-extra-attribute](https://quan-images.b-cdn.net/blogs/2026/09/django-extra-attribute.png) People often miss the detail that the core power of an LSP server comes from the static type checker (or the compiler in case of compiled languages). When you write `window.` then hit the `Tab` key, the LSP server starts to suggest the attributes of the `window` object. But it can only know which attributes the `window` object has, if the type of the `window` variable is inferred successfully and correctly. The later job is done by static type checker, or concretely, Mypy. So, for a Django project, how do you set up Mypy so type inference works? Basically, it's just like adding Mypy for type checking, but with more honesty. I use the word "honesty" because some projects added Mypy but the team members try to get the green "passed" status by making Mypy skip the type checking, ignoring the errors instead of really fixing them. You will need to add both `django-stubs` and `django-stubs-ext` as dependencies. If your project has *pyproject.toml* file, `django-stubs-ext` will go in the main dependency list and `django-stubs` just needs to be in the same dependency group as `mypy`, in my case, that's the `dev` group. ```toml dependencies = [ "django >= 6.1", "django-stubs-ext >= 6.1", ... ] [dependency-groups] dev = [ "mypy >= 2.3.1", "django-stubs >= 6.1", "djangorestframework-stubs >= 3.18.0", ... ] ``` (In 2026, if you tell an AI assistant to scaffold a Python project, it will define "dev" as a group in `[project.optional-dependencies]` instead of `[dependency-groups]`. That is wrong) Then enable the *django-stubs* plugin for Mypy: ```toml [tool.mypy] python_version = "3.14" plugins = [ "mypy_django_plugin.main", "mypy_drf_plugin.main", ] warn_redundant_casts = true warn_unused_ignores = true ``` Some Python packages do not provide type info yet; we can tell Mypy to ignore them: ```toml [[tool.mypy.overrides]] module = [ "django_filters.*", "anymail.*", ] ignore_missing_imports = true ``` but keep an eye on this list. If a package is updated with type info added, remove it from this "ignore" list. When writing Python code, we may forget to add type annotations, which weakens Mypy. We can let Ruff remind us by adding [ANN] rules to the Ruff config. ```toml [tool.ruff.lint] select = ["E4", "E7", "E9", "F", "I", "BLE001", "UP", "ANN201", "ANN205"] ignore = ["UP040", "UP046", "UP047", "UP049"] ``` If you are using Python older than 3.14, you should get used to the `from __future__ import annotations` and add this to Ruff lint config: ```toml [tool.ruff.lint] ... # Enable `from __future__ import annotations` imports future-annotations = true ``` Using this *future* feature of type annotation will save us from the headache of circular imports when you just need the type annotation but have to import classes at runtime. One more step is to add this line to the *settings.py* file. ```py import django_stubs_ext django_stubs_ext.monkeypatch() ``` Note that adding Mypy to the project does not automatically make type inference work. It's only half the job. The other half is that you have to add type annotations to functions and object methods. This part is pretty difficult and though I tried so hard to make my projects pass the Mypy check, I can't confidently say that I can resolve all the cases. If you plan to let AI assistants do this task, be aware that most of the time, AI assistants will try to suppress errors by `type: ignore`, or use `Any` type instead of doing a real fix, and that doesn't help Mypy or `pylsp` at all. If your project depends on `pydantic`, you will need to enable more Mypy rules and add more type annotations. For example, the `disallow_any_generics = true` rule will force you to define `admin` like this: ```py class TeamAdmin(admin.ModelAdmin[Team]): ... ``` If you find it difficult to define some types, sometimes you can read the source code of *django-stubs* and copy its implementation. For example, my project needs a custom `ArrayField` and I define it as: ```py from typing import TypeVar from django.contrib.postgres.fields import ArrayField from django.forms.widgets import Widget # Ref: https://github.com/typeddjango/django-stubs/blob/master/django-stubs/contrib/postgres/fields/array.pyi # __set__ value type _ST = TypeVar('_ST') # __get__ return type _GT = TypeVar('_GT') class _TypedMultipleChoiceField(forms.TypedMultipleChoiceField): def __init__(self, *args, **kwargs) -> None: kwargs.pop('base_field', None) kwargs.pop('max_length', None) # We will overwrite label to prevent it from being rendered kwargs['label'] = '' super().__init__(*args, **kwargs) class ChoiceArrayField(ArrayField[_ST, _GT]): """ A field that allows us to store an array of choices. Uses Django 4.2's postgres ArrayField and a `TypedMultipleChoiceField` for its form field. Usage: choices = ChoiceArrayField( models.CharField(max_length=..., choices=(...,)), blank=[...], default=[...] ) """ def formfield( self, form_class: type[forms.Field] | None = None, choices_form_class: type[forms.ChoiceField] | None = None, required: bool = False, widget: Widget | type[Widget] | None = None, **kwargs, ) -> forms.Field | None: if not widget: widget = FilteredSelectMultiple(self.verbose_name, is_stacked=False) form_class = form_class or _TypedMultipleChoiceField choices_form_class = choices_form_class or forms.MultipleChoiceField return super().formfield( form_class=form_class, choices_form_class=choices_form_class, required=required, widget=widget, choices=self.base_field.choices, coerce=self.base_field.to_python, **kwargs, ) ``` Here, the `Field[_ST, _GT]` pattern is just copied from `django-stubs` source. [ANN]: https://docs.astral.sh/ruff/rules/#flake8-annotations-ann [django-stubs]: https://github.com/typeddjango/django-stubs