Contribution Guide
July 24, 2026 · View on GitHub
This project is open source and community driven. As such we encourage code contributions of all kinds. Some areas you can contribute in:
- Improve the stubs
- Sync stubs with the latest version of Django
- Improve plugin code and extend its capabilities
- Write tests
- Update dependencies
- Fix and remove things from our
scripts/stubtest/allowlist_todo.txt
Tutorials
If you want to start working on this project, you will need to get familiar with python typings. The Mypy documentation offers an excellent resource for this, as well as the python official documentation:
Additionally, the following resources might be useful:
- How to write custom mypy plugins
- Typechecking Django and DRF guide
- Testing mypy stubs, plugins, and types guide
- Awesome Python Typing list
Dev setup
Repository Setup
As a first step you will need to fork this repository and clone your fork locally. In order to be able to continuously sync your fork with the origin repository's master branch, you will need to set up an upstream master. To do so follow this official github guide.
System Dependencies
The test suite requires some system libraries that Django itself treats as optional.
This project depends on mysqlclient, which needs MySQL/MariaDB C client libraries to build.
Install them for your platform following the
mysqlclient install guide.
For Debian/Ubuntu, the django-docker-box packages list
is also a useful reference.
GDAL and GEOS are needed to pass all tests (2 GIS-related tests require them). See the Django documentation on installing geospatial libraries. If you're not working on GIS-related stubs, you can skip GDAL/GEOS — the 2 failing tests won't affect other contributions.
macOS Note: Homebrew installs GDAL/GEOS to
/opt/homebrew(Apple Silicon) or/usr/local(Intel), which are not in the default library search path. The GIS tests may fail unless you create symlinks:sudo mkdir -p /usr/local/lib sudo ln -s /opt/homebrew/opt/gdal/lib/libgdal.dylib /usr/local/lib/libgdal.dylib sudo ln -s /opt/homebrew/opt/geos/lib/libgeos_c.dylib /usr/local/lib/libgeos_c.dylib
Dependency Setup
We use uv to manage our dev dependencies. To install it, see their installation guide
We use just as a command runner. Install it with:
uv tool install rust-just
Then bootstrap your environment (installs pre-commit hooks and syncs dependencies):
just bootstrap
Manual setup (without just)
uv sync
source .venv/bin/activate
pre-commit install --install-hooks
Testing and Linting
Running just at the root of the repository lists all available recipes
Before submitting a PR, run all checks at once:
just pre-mr-check
Extra arguments can be passed to test and stubtest:
just test tests -k test_name
just stubtest --allowlist extra.txt
If you get unexpected results, clear the mypy cache with just clean.
Model-based assert_type tests
For tests that need Django models, add a models.py module under tests/assert_type/.
Its package is discovered and automatically added to INSTALLED_APPS for the test suite.
To keep a test self-contained, put both the models and the assert_type assertions in
that single models.py. Write the assertions inside functions, they are never executed and only statically checked,
so they can safely reference the app registry:
# tests/assert_type/apps/test_registry/models.py
from __future__ import annotations
from django.apps import apps
from django.db import models
from typing_extensions import assert_type
class First(models.Model):
pass
def get_model_resolves_literal_references() -> None:
# only the mypy plugin resolves the reference; the stubs return `type[Any]`, so the
# other type checkers need an inline ignore for the `assert_type` mismatch
assert_type(apps.get_model("test_registry.First"), type[First]) # pyright: ignore[reportAssertTypeFailure] # pyrefly: ignore[assert-type] # ty: ignore[type-assertion-failure]
tests/assert_type/
└── apps/test_registry/
├── __init__.py
└── models.py
Debugging plugin code
For yml tests, we use a dedicated pytest plugin that is by default
running mypy in a subprocess, making it difficult to debug.
To avoid that, run pytest with the --mypy-same-process flag.
pytest --mypy-same-process tests/typecheck/managers/querysets/test_annotate.yml
Testing stubs with stubtest
Run just stubtest (or ./scripts/stubtest.sh directly) to test that stubs and sources are in-line.
We have some special files to allow errors:
scripts/stubtest/allowlist.txt- Permanent exclusions, e.g. internal utilities, plugin-handled items, type system limitations.scripts/stubtest/allowlist_todo.txt- General backlog of stub errors to fix or move to permanent allowlist.
You might also want to disable incremental mode while working on stubtest changes.
This mode leads to several known problems (stubs do not show up or have strange errors).
Submission Guidelines
The workflow for contributions is fairly simple:
- Fork and set up the repository as in the previous step.
- Create a local branch.
- Make whatever changes you want to contribute.
- Ensure your contribution passes linting and tests.
- Make a pull request with an adequate description.
Generics
As Django uses a lot of the more dynamic features of Python (i.e. metaobjects), statically typing it requires heavy use of generics.
Unfortunately, the syntax for generics is also valid Python syntax.
For instance, the statement class SomeClass(SuperType[int]) implicitly translates to class SomeClass(SuperType.__class_getitem__(int)).
If SuperType doesn't define the __class_getitem__ method, this causes a runtime error, even if the code passes type checking.
When adding a new generic class, or changing an existing class to use generics, the tests/test_generic_consistency.py test checks that the class (or one of its bases) is patched in the django_stubs_ext.patch module.
If it fails, please add the new generic class to the _need_generic list there.
Private attributes
We only add hints for private attributes when it has some demonstrated real-world use case. That means from a third-party package or some well described snippet for a project. This rule helps us avoid tying in too closely to Django’s undocumented internals.
Releasing django-stubs
-
Open a pull request that updates
pyproject.toml,ext/pyproject.tomlandREADME.md(anyone can open this PR, not just maintainers):-
Version number
major.minor.patchis formed as follows:major.minorversion must match newest supported Django release.patchis sequentially increasing for each stubs release. Reset to0ifmajor.minorwas updated. -
Update the
version =value within[project]section in bothpyproject.tomlfiles. The versions must be in sync. -
Update
django-stubs-ext>=dependency in rootpyproject.tomlto the same version number. -
Run
uv lockto update lockfile -
Add a new row at the top of 'Version compatibility' table in README.md.
-
Use pull request title "Version x.y.z release" by convention.
-
Add the correct classifiers to
classifiers =if support is added for a new Python or Django version
-
-
Ensure the CI succeeds. A maintainer must merge this PR. If it's just a version bump, no need to wait for a second maintainer's approval.
-
A maintainer must сreate a new GitHub release:
- Under "Choose a tag" enter the new version number. Do not use
vprefix. - Click "Generate release notes".
- Look for merged PRs with the 'release notes reminder' label and move them to a separate section at the top, so that they stand out. Remove the label from PRs.
- Delete all release notes lines containing
by @pre-commit-ciorby @dependabot, as these are irrelevant for our users.
- Under "Choose a tag" enter the new version number. Do not use
-
Once you feel brave enough, click "Publish release".
-
Check that the release workflow succeeds.