Reusable Apps & Third-Party Packages
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Django 6.1 (6.1.1 on PyPI, released 2026-09-02; 6.1.2 is scheduled for 2026-10-06), Python
3.12-3.14 and PostgreSQL 15 or newer, with the Django 5.2 LTS differences noted where they matter; every version number
was checked on 2026-10-03. Every listing was run in a scratch project on Python 3.13 and Django 6.1.1 with SQLite: the
example app was built into a wheel and an sdist with both uv build (uv 0.12.22) and python -m build, installed into a
separate project, migrated, served with runserver and fetched with curl; its own test suite passed under Django
5.2.17, 6.0.8 and 6.1.1 with warnings turned into errors; and the third-party packages in the ecosystem table that are
plain Django apps were installed together and passed manage.py check. Django’s tutorial ends with two steps that this
page expands: part 8 adds a community package (the Debug Toolbar) to a project, and the "advanced tutorial" turns the
tutorial’s polls app into an installable package. The Bookshelf application that grows across this section uses both
directions: it consumes packages such as django-allauth and WhiteNoise, and it extracts its star-rating feature into a
reusable app, django-book-ratings, that another project can pip install.
In a hurry: what goes where
| Need | Rule of thumb | Section |
|---|---|---|
Make an app reusable |
Everything the app needs lives inside its package: models, migrations, templates, static files, URLs, admin, checks. Nothing in it imports the project. |
|
Name the app |
Distribution |
|
Ship templates and static files |
|
|
Ship migrations |
Generate them from a minimal test settings module; point at the user model with |
|
Test the app without a project |
A |
|
Describe the package |
|
|
Build and install it |
|
|
Promise compatibility |
|
|
Add someone else’s package |
Install it, then follow its installation guide ( |
|
Choose between packages |
Django Packages grids, the well-maintained test, classifiers and release dates on PyPI. |
|
Know the usual suspects |
The curated table of community packages, each linked to its docs and to the page of this section that uses it. |
What makes an app reusable
A Django project is a settings module, a root URLconf and the deployment around them; an app is a Python package that
provides one feature to a project (see Project Structure & Apps). Every
app is reusable in principle, because a project only refers to it by its import path in INSTALLED_APPS. In practice an
app becomes reusable when it stops depending on the project around it:
-
it owns its models, migrations, templates, static files, URLconf, admin registrations, forms and system checks, all inside its own package directory;
-
it never imports from the project (
config.settings, another local app) and reads configuration only throughdjango.conf.settings, with documented defaults; -
it refers to the user model through
settings.AUTH_USER_MODELandget_user_model()rather thandjango.contrib.auth.models.User, so it works with a custom user model; -
its templates extend a base template the project is expected to provide, or none at all, and every template and static file sits in a directory named after the app so the project can override it;
-
it ships a test suite that runs without any project.
The official advanced tutorial starts from the tutorial’s project and observes that the polls directory could already
be copied into another project and work; what it lacks is packaging. A copied directory has no version, no declared
dependencies and no upgrade path, so the rest of this page turns the Bookshelf ratings feature into a proper
distribution.
inside the project"] --> B["django-book-ratings/
pyproject.toml + django_book_ratings/"] B --> C["runtests.py
tests pass on 5.2, 6.0, 6.1"] C --> D["uv build / python -m build"] D --> E["dist/*.whl + dist/*.tar.gz"] E --> F["pip install / uv pip install
in any project"] E --> G["upload to PyPI
or a private index"] F --> H["INSTALLED_APPS + include()
migrate"]
The package layout
Create the package in its own directory outside any project (and, eventually, in its own repository). The tutorial’s
naming advice is the convention the whole ecosystem follows: check PyPI for a free name, use a django- prefix for the
distribution and the matching django_ prefix for the import package, exactly as django-ratelimit provides the
django_ratelimit module.
django-book-ratings/
├── LICENSE
├── MANIFEST.in
├── README.rst
├── pyproject.toml
├── runtests.py
├── django_book_ratings/
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── models.py
│ ├── urls.py
│ ├── views.py
│ ├── migrations/
│ │ ├── __init__.py
│ │ ├── 0001_initial.py
│ │ └── 0002_rating_rated_by.py
│ ├── static/
│ │ └── book_ratings/
│ │ └── ratings.css
│ └── templates/
│ └── book_ratings/
│ └── rating_list.html
└── tests/
├── __init__.py
├── test_settings.py
├── test_ratings.py
└── urls.py
The tests/ package sits next to the app, not inside it, so it is not installed into users' environments. The
README.rst doubles as the long description on PyPI and should contain the quick start a user needs: what to add to
INSTALLED_APPS, which URLconf to include and that migrate is required. Code published without a LICENSE cannot
legally be reused; Django itself and most of its ecosystem use the BSD licence.
AppConfig for a reusable app
The app’s AppConfig is the one place where a reusable app pins down how projects will see it.
from django.apps import AppConfig
class BookRatingsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "django_book_ratings"
label = "book_ratings"
verbose_name = "Book ratings"
-
nameis the full Python path of the package and must match the import package, heredjango_book_ratings. -
labeldefaults to the last component ofname. It is what appears in migration names (book_ratings.0001_initial), database table names (book_ratings_rating), permission codenames,app_labellookups and admin URLs, so it must be unique inINSTALLED_APPSand should not reuse a contrib label such asauth,adminormessages. Choose it once: changing a label after release orphans every user’s migration history and tables. -
default_auto_fieldfixes the primary-key type of the app’s models. Without it the app follows the project’sDEFAULT_AUTO_FIELD, so two projects would generate different migrations from the same models and your shipped migrations would no longer match. Setting it on theAppConfigis exactly what the tutorial recommends. -
verbose_nameis the human-readable name shown in the admin.
Because the package has exactly one AppConfig subclass in apps.py, Django selects it automatically, and users can
write either "django_book_ratings" or "django_book_ratings.apps.BookRatingsConfig" in INSTALLED_APPS. If an app
needs start-up work (registering signal handlers or system checks), it does so in AppConfig.ready() as described in
How apps are loaded and what ready() is for.
The model below is the whole feature. It refers to users through settings.AUTH_USER_MODEL, so it works whatever user
model the project chose.
from django.conf import settings
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
class Rating(models.Model):
book_title = models.CharField(max_length=200)
stars = models.PositiveSmallIntegerField(
validators=[MinValueValidator(1), MaxValueValidator(5)]
)
created = models.DateTimeField(auto_now_add=True)
rated_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="book_ratings",
)
class Meta:
ordering = ["-created"]
def __str__(self):
return f"{self.book_title}: {self.stars}/5"
Source: Referencing the User model
Namespacing templates, static files and URLs
Django searches templates and static files across all installed apps and uses the first match. The app-directories
template loader looks in each app’s templates/ directory in INSTALLED_APPS order, after the project’s DIRS; the
app-directories static finder does the same with each app’s static/. If two apps both shipped templates/list.html or
static/style.css, one would silently shadow the other. The fix is to put every file one level deeper, in a directory
named after the app label: templates/book_ratings/ and static/book_ratings/.
{% load static %}
<link rel="stylesheet" href="{% static 'book_ratings/ratings.css' %}">
<h1>Ratings</h1>
<ul>
{% for rating in ratings %}
<li class="rating">{{ rating }}</li>
{% empty %}
<li>No ratings yet.</li>
{% endfor %}
</ul>
The view renders the namespaced name, and the app’s URLconf declares an app_name so that reversing never clashes with
another app’s "list" URL (see URLs & Routing).
# views.py
from django.shortcuts import render
from .models import Rating
def rating_list(request):
ratings = Rating.objects.all()[:20]
return render(request, "book_ratings/rating_list.html", {"ratings": ratings})
# urls.py
from django.urls import path
from . import views
app_name = "book_ratings"
urlpatterns = [
path("", views.rating_list, name="list"),
]
Source: How to manage static files: Static file namespacing and Tutorial part 3: Template namespacing and Tutorial part 3: Namespacing URL names
Namespacing is also what makes the app customisable. A project that keeps a templates/ directory in DIRS can
replace any of the app’s templates by creating a file with the same relative name; the project directory is searched
first, so the override wins without touching the installed package.
<h1>Bookshelf ratings</h1>
{% for rating in ratings %}<p>{{ rating.book_title }} ({{ rating.stars }} stars)</p>{% endfor %}
$ curl -s http://127.0.0.1:8000/ratings/
<h1>Bookshelf ratings</h1>
<p>Dune (5 stars)</p>
Keep the overridable surface small and documented: a reusable app usually offers one base.html of its own that its
other templates extend, with named blocks the project can fill, rather than forcing the project to copy every template.
Templates, tags and filters are covered in depth in Templates; static files and their
deployment in Static Files & Front-End Assets.
Migrations shipped with the app
A reusable app ships its migrations inside the package; users run migrate, never makemigrations, for your app. The
migration files are therefore part of the app’s public contract (the migration mechanics themselves are covered in
Migrations):
-
generate them from the app’s own minimal settings, not from a real project, so nothing project-specific leaks in;
-
refer to the user model through
settings.AUTH_USER_MODELin the model, which makesmakemigrationsemit aswappable_dependencyinstead of a hard dependency onauth; -
never edit or renumber a migration once it is released: add a new one, as
0002_rating_rated_bydoes in version 0.2.0; -
keep a migration for every model change, and fail CI if one is missing (
makemigrations --check); -
data migrations must use the historical models from
apps.get_model(), never imports frommodels.py.
$ cd django-book-ratings
$ DJANGO_SETTINGS_MODULE=tests.test_settings PYTHONPATH=. django-admin makemigrations book_ratings --name rating_rated_by
Migrations for 'book_ratings':
django_book_ratings/migrations/0002_rating_rated_by.py
+ Add field rated_by to rating
$ DJANGO_SETTINGS_MODULE=tests.test_settings PYTHONPATH=. django-admin makemigrations --check --dry-run
No changes detected
makemigrations takes the app label (book_ratings), not the package name; with django_book_ratings it answers "No
installed app with label 'django_book_ratings'. Did you mean 'book_ratings'?". The generated migration shows the swappable
dependency:
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('book_ratings', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='rating',
name='rated_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='book_ratings', to=settings.AUTH_USER_MODEL),
),
]
When the migration history grows long, squash it (squashmigrations) and keep the squashed and original files side by
side for at least one release, so users who are part-way through the old chain can still upgrade; delete the originals
only in a later release that requires users to have applied the squash.
Testing the app without a project
Django’s testing docs describe the standard layout for a reusable app: a tests package next to the app, a settings
module containing only what the tests need, and a runtests.py that boots Django and calls the configured test runner.
#!/usr/bin/env python
import os
import sys
import django
from django.conf import settings
from django.test.utils import get_runner
if __name__ == "__main__":
os.environ["DJANGO_SETTINGS_MODULE"] = "tests.test_settings"
django.setup()
TestRunner = get_runner(settings)
test_runner = TestRunner()
failures = test_runner.run_tests(["tests"])
sys.exit(bool(failures))
# tests/test_settings.py -- only for the test suite, never for a deployment
from django.core.management.utils import get_random_secret_key
SECRET_KEY = get_random_secret_key() # throw-away key for the test run
INSTALLED_APPS = [
"django.contrib.contenttypes",
"django.contrib.auth",
"django.contrib.staticfiles",
"django_book_ratings",
]
DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}}
ROOT_URLCONF = "tests.urls"
STATIC_URL = "static/"
TEMPLATES = [
{"BACKEND": "django.template.backends.django.DjangoTemplates", "APP_DIRS": True}
]
USE_TZ = True
# tests/urls.py
from django.urls import include, path
urlpatterns = [path("ratings/", include("django_book_ratings.urls"))]
# tests/test_ratings.py
from django.test import TestCase
from django.urls import reverse
from django_book_ratings.models import Rating
class RatingListTests(TestCase):
def test_lists_ratings(self):
Rating.objects.create(book_title="Dune", stars=5)
response = self.client.get(reverse("book_ratings:list"))
self.assertContains(response, "Dune: 5/5")
self.assertTemplateUsed(response, "book_ratings/rating_list.html")
$ python -W error runtests.py
Found 1 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 1 test in 0.005s
OK
Destroying test database for alias 'default'...
The hardcoded SECRET_KEY is acceptable only because this module configures a throw-away test run with an in-memory
database; real projects read it from the environment (see
Settings & Configuration). Running with -W error turns Django’s
deprecation warnings into failures, which is the cheapest early warning that the app uses an API scheduled for removal.
pytest users get the same effect with pytest-django and a DJANGO_SETTINGS_MODULE entry in their configuration (see
pytest, Factories & Browser Tests).
Describing the package in pyproject.toml
pyproject.toml is the single, tool-independent description of a Python distribution. Its [build-system] table names
the build backend that turns the source tree into a wheel and an sdist; its [project] table holds the metadata that
ends up on PyPI and that installers use to resolve dependencies. The official tutorial uses setuptools as the backend, and
so does the listing below; it is the tutorial’s file filled in for a real app.
[build-system]
requires = ["setuptools>=83"]
build-backend = "setuptools.build_meta"
[project]
name = "django-book-ratings"
version = "0.2.0"
description = "A reusable Django app that stores star ratings for books."
readme = "README.rst"
license = "BSD-3-Clause"
license-files = ["LICENSE"]
requires-python = ">=3.12"
authors = [{ name = "Your Name", email = "yourname@example.com" }]
dependencies = [
"Django>=5.2",
]
classifiers = [
"Environment :: Web Environment",
"Framework :: Django",
"Framework :: Django :: 5.2",
"Framework :: Django :: 6.0",
"Framework :: Django :: 6.1",
"Intended Audience :: Developers",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
]
[project.urls]
Homepage = "https://example.com/django-book-ratings/"
Changelog = "https://example.com/django-book-ratings/changelog/"
[tool.setuptools.packages.find]
include = ["django_book_ratings*"]
Source: Advanced tutorial: Packaging your app (pyproject.toml) and Python Packaging User Guide: Writing your pyproject.toml
What each part does:
-
[build-system]—requiresis installed into an isolated build environment, thenbuild-backendis called. The tutorial pinssetuptools>83; any recent setuptools understands the SPDXlicenseexpression andlicense-files. -
nameandversion— the distribution name userspip install, and the version they pin. The import name (django_book_ratings) is independent of it. -
license— an SPDX expression (BSD-3-Clause), with the licence text attached throughlicense-files. This PEP 639 form supersedes the olderLicense ::classifiers, so do not add both. -
requires-python— installers skip the release on older interpreters. Match it to the oldest Python your oldest supported Django accepts and you test on. -
dependencies— the runtime requirements. A lower bound on Django is the norm; see Versioning and Django version support for why an upper bound is usually left out. -
classifiers— searchable labels on PyPI.Framework :: Djangoplus oneFramework :: Django :: X.Yper supported feature release is how users (and Django Packages) learn which Django versions you test against. -
[tool.setuptools.packages.find]— limits automatic discovery to the app package, so the top-leveltestspackage is not installed as an importabletestsmodule into users' environments.
Setuptools only puts non-Python files into the package when they are listed as package data, so the tutorial adds a
MANIFEST.in for templates and static files:
recursive-include django_book_ratings/templates *
recursive-include django_book_ratings/static *
This file is the most common reason a freshly published app fails with TemplateDoesNotExist. In the scratch project,
building the same tree without MANIFEST.in (and outside a Git checkout, so no VCS plugin could add the files)
produced a wheel with Python modules and migrations but with zero templates and static files.
Alternative: the uv build backend
Any standards-compliant backend works, because installers only talk to it through the [build-system] interface.
uv ships its own backend, uv_build, which includes every file under the module directory (templates and static files
included), so no MANIFEST.in is needed. Its default layout expects the package under src/; for the flat layout used
by the tutorial, set module-root to an empty string:
[build-system]
requires = ["uv_build>=0.12,<0.13"]
build-backend = "uv_build"
[tool.uv.build-backend]
module-name = "django_book_ratings"
module-root = ""
Source: uv: The uv build backend
The upper bound on uv_build is the form uv’s documentation recommends, because the backend’s configuration may change
between minor versions. Hatchling, Flit and PDM backends are equally valid; whichever you pick, inspect the built wheel
once (next section) before you publish.
Building, inspecting and installing the package
Either build frontend reads [build-system], creates an isolated environment with the backend and produces both
distribution formats in dist/: a source distribution (.tar.gz) and a pure-Python wheel (py3-none-any.whl).
$ cd django-book-ratings
$ uv build
...
Successfully built dist/django_book_ratings-0.2.0.tar.gz
Successfully built dist/django_book_ratings-0.2.0-py3-none-any.whl
$ python -m pip install build # once, if you use the PyPA frontend instead
$ python -m build
...
Successfully built django_book_ratings-0.2.0.tar.gz and django_book_ratings-0.2.0-py3-none-any.whl
Source: Advanced tutorial: Packaging your app (python -m build), uv: Building distributions and build
A wheel is a zip file, so its contents can be checked with the standard library before anything is published. The listing must contain the migrations, the templates and the static files:
$ python -m zipfile -l dist/django_book_ratings-0.2.0-py3-none-any.whl | awk '{print $1}'
File
django_book_ratings/__init__.py
django_book_ratings/admin.py
django_book_ratings/apps.py
django_book_ratings/models.py
django_book_ratings/urls.py
django_book_ratings/views.py
django_book_ratings/migrations/0001_initial.py
django_book_ratings/migrations/0002_rating_rated_by.py
django_book_ratings/migrations/__init__.py
django_book_ratings/static/book_ratings/ratings.css
django_book_ratings/templates/book_ratings/rating_list.html
django_book_ratings-0.2.0.dist-info/licenses/LICENSE
django_book_ratings-0.2.0.dist-info/METADATA
django_book_ratings-0.2.0.dist-info/WHEEL
django_book_ratings-0.2.0.dist-info/top_level.txt
django_book_ratings-0.2.0.dist-info/RECORD
Source: zipfile: Command-Line Interface
The METADATA file is what PyPI and installers read; the classifiers and the Django requirement from pyproject.toml
appear there verbatim:
$ unzip -p dist/django_book_ratings-0.2.0-py3-none-any.whl '*/METADATA' | grep -E '^(Requires|Classifier: Framework)'
Classifier: Framework :: Django
Classifier: Framework :: Django :: 5.2
Classifier: Framework :: Django :: 6.0
Classifier: Framework :: Django :: 6.1
Requires-Python: >=3.12
Requires-Dist: Django>=5.2
Source: Core metadata specifications
Now install the wheel into a separate project’s virtual environment (the tutorial’s pip install --user works too, but a
virtual environment is the robust choice the tutorial itself recommends at the end), and wire it in exactly as the
README says:
$ cd ../bookshelf
$ uv pip install ../django-book-ratings/dist/django_book_ratings-0.2.0-py3-none-any.whl
+ django-book-ratings==0.2.0
# or: python -m pip install ../django-book-ratings/dist/django_book_ratings-0.2.0-py3-none-any.whl
# config/settings.py
INSTALLED_APPS = [
# ... the django.contrib apps ...
"django_book_ratings",
]
# config/urls.py
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path("admin/", admin.site.urls),
path("ratings/", include("django_book_ratings.urls")),
]
$ python -W error manage.py check
System check identified no issues (0 silenced).
$ python manage.py migrate
...
Applying book_ratings.0001_initial... OK
Applying book_ratings.0002_rating_rated_by... OK
$ python manage.py findstatic book_ratings/ratings.css
Found 'book_ratings/ratings.css' here:
.../site-packages/django_book_ratings/static/book_ratings/ratings.css
$ python manage.py shell -c "from django_book_ratings.models import Rating; Rating.objects.create(book_title='Dune', stars=5)"
$ python manage.py runserver
$ curl -s http://127.0.0.1:8000/ratings/
<link rel="stylesheet" href="/static/book_ratings/ratings.css">
<h1>Ratings</h1>
<ul>
<li class="rating">Dune: 5/5</li>
</ul>
Source: Advanced tutorial: Using your own package and Installing Python packages with a virtual environment
Upgrading is the same: install the 0.2.0 wheel over 0.1.0 and run migrate, which applies only the new
0002_rating_rated_by migration. During development, an editable install (uv pip install -e ../django-book-ratings or
python -m pip install -e ../django-book-ratings) makes the project use the working tree directly, so edits take effect
without rebuilding.
Publishing means uploading the files in dist/ to PyPI or to a private index. The Python Packaging User Guide walks
through it with TestPyPI first; uv publish and twine upload are the usual commands, and PyPI’s trusted publishing
lets a CI workflow upload without a long-lived API token stored anywhere.
Versioning and Django version support
A reusable app makes two promises in its metadata: its own version number, and the Django and Python versions it works with. Both should follow from Django’s own release process (summarised in Introduction & Architecture).
Your version number. Most Django packages use semantic-style MAJOR.MINOR.PATCH versions, and a few (django-filter
26.1, crispy-bootstrap5 2026.9) use calendar versions; either is fine as long as the changelog says which releases contain
migrations, breaking changes or dropped Django versions. Dropping a Django or Python version is a breaking change for the
users still on it, so do it in a release whose number makes that visible and say so in the changelog.
Which Django versions to support. Support every Django release that is still supported upstream. On 2026-10-03 that
is 5.2 LTS (extended support until April 2028), 6.0 (until April 2027) and 6.1 (until December 2027); 6.2 LTS follows in
April 2027. Django’s deprecation policy explains why the LTS matters to app authors: a feature deprecated in one release
keeps working, with a warning, for at least two feature releases, and the policy explicitly asks third-party apps to
stay compatible back to the previous LTS so users can upgrade LTS to LTS. Django 6.1 also adopted calendar versioning:
after 6.2 the next releases are Django 2028 and Django 2029, and the deprecation warning classes follow
(RemovedInDjango2028Warning).
How to declare it.
-
dependencies = ["Django>=5.2"]— a lower bound equal to the oldest version you test. -
One
Framework :: Django :: X.Yclassifier per tested feature release, updated when you add a release to the test matrix. -
No upper bound (
Django<6.2) by default. Because deprecations are announced two releases ahead, an app tested with warnings as errors usually works on the next release unchanged, and an upper bound would block users from upgrading Django until you publish a release. Packages that do need a cap, for example because they patch Django internals, should document why. -
requires-pythonmatching the oldest Python that both your oldest Django and your test matrix use: Django 5.2 accepts Python 3.10-3.14, Django 6.0 and 6.1 accept 3.12-3.14, so an app that requires 3.12 still serves 5.2 users who are on 3.12 or newer.
How to prove it. Run the suite once per supported Django release, with deprecation warnings turned into errors. A CI matrix does this in production; locally, uv can run each combination in a throw-away environment:
$ for django in "5.2.*" "6.0.*" "6.1.*"; do
> uv run --isolated --no-project -p 3.13 --with "django==$django" python -W error runtests.py > /dev/null 2>&1
> echo "Django $django: exit $?"
> done
Django 5.2.*: exit 0
Django 6.0.*: exit 0
Django 6.1.*: exit 0
Source: Django’s release process: Deprecation policy, Download: Supported versions and Django 6.1 release notes
tox and nox are the traditional tools for the same matrix, and django-upgrade (1.33.0) rewrites code that uses
deprecated Django APIs, which keeps the warnings-as-errors run green when a new release lands. The API stability promise
that makes all of this possible is documented in API stability.
Adding a third-party package (tutorial part 8)
Part 8 of the tutorial is about the other direction: using an app someone else packaged. Its example is Django Debug Toolbar, maintained by the Django Commons organisation and consistently among the most used packages in the Django Developers Survey. The general recipe has two halves:
-
install the distribution into the project’s virtual environment, and record it in the project’s dependency file (
pyproject.toml, a lock file or a requirements file); -
integrate it by following the package’s own installation guide, which usually means an
INSTALLED_APPSentry and often URLs, middleware and settings.
The tutorial deliberately does not copy the toolbar’s steps, because a third-party package changes on its own schedule. For django-debug-toolbar 8.0.0 (2026-09-01, classified for Django 5.2, 6.0 and 6.1) its installation guide asks for the following, which the scratch project ran on Django 6.1.1:
$ python -m pip install django-debug-toolbar # or: uv add --dev django-debug-toolbar
# config/settings.py -- staticfiles and APP_DIRS are already in the startproject template
INSTALLED_APPS = [
# ...
"django.contrib.staticfiles",
"django_book_ratings",
"debug_toolbar",
]
MIDDLEWARE = [
"debug_toolbar.middleware.DebugToolbarMiddleware", # as early as possible
# ... the rest of the startproject middleware ...
]
INTERNAL_IPS = ["127.0.0.1"]
# config/urls.py
from debug_toolbar.toolbar import debug_toolbar_urls
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path("admin/", admin.site.urls),
path("ratings/", include("django_book_ratings.urls")),
] + debug_toolbar_urls()
$ python -W error manage.py check
System check identified no issues (0 silenced).
$ curl -s http://127.0.0.1:8000/admin/login/ | grep -c djDebug
64
The middleware must come after any middleware that encodes the response, such as GZipMiddleware; the toolbar only
appears for addresses in INTERNAL_IPS; and it belongs in development settings only, never in production. The toolbar
itself is used to find duplicated queries in
Query Optimization & Fetch Modes, and the order of middleware is
explained in Middleware.
When an installed package misbehaves, the tutorial’s advice applies to every package: read its troubleshooting or tips page, search its issue tracker for the same symptom, then ask on the Django Forum or the Django Discord server.
Evaluating packages before you depend on them
Every dependency is code you ship, upgrade and secure. The tutorial points to two tools for choosing.
Django Packages (https://djangopackages.org/) is the community directory of reusable apps. Its grids compare the packages that solve the same problem (authentication, REST frameworks, CMSs, task queues…) side by side (stars, Python 3 support, recent commit activity and more), so a grid is the right first stop when you need "a package for X".
The well-maintained test, a list of yes/no questions by Adam Johnson that the tutorial links, turns "is it maintained?" into concrete signals; each "yes" scores a point and you decide the pass mark from your risk tolerance. In summary, it asks whether the package:
-
is described as production ready (no alpha/beta wording or pre-release version);
-
has sufficient documentation and a changelog that covers recent releases;
-
has someone responding to bug reports;
-
has tests, run in CI that is passing, against the latest Python and the latest Django;
-
seems relatively well used;
-
has had a commit and a release in the last year.
On top of the test, a maintainer team rather than a single person (an organisation such as Django Commons or Jazzband is a good sign) and a documented way to report security issues both lower the risk of being stranded.
PyPI’s JSON API exposes several of these signals, so they can be checked in a few lines before adding a dependency:
import json
import sys
from urllib.request import urlopen
def summarise(name):
with urlopen(f"https://pypi.org/pypi/{name}/json", timeout=10) as response:
data = json.load(response)
info = data["info"]
files = data["releases"].get(info["version"], [])
uploaded = files[0]["upload_time_iso_8601"][:10] if files else "unknown"
django = [
c.rsplit("::", 1)[1].strip()
for c in info["classifiers"]
if c.startswith("Framework :: Django :: ")
]
print(f"{info['name']} {info['version']} (uploaded {uploaded})")
print(f" requires-python: {info['requires_python']}")
print(f" Django classifiers: {', '.join(django) or 'none'}")
status = [
c.split(" :: ")[-1]
for c in info["classifiers"]
if c.startswith("Development Status")
]
print(f" development status: {', '.join(status) or 'not declared'}")
if __name__ == "__main__":
for package in sys.argv[1:]:
summarise(package)
$ python check_package.py django-filter django-extensions
django-filter 26.1 (uploaded 2026-07-11)
requires-python: >=3.10
Django classifiers: 5.2, 6.0, 6.1
development status: 5 - Production/Stable
django-extensions 4.1 (uploaded 2025-04-11)
requires-python: >=3.9
Django classifiers: 4.2, 5.1, 5.2
development status: 5 - Production/Stable
Source: Tutorial part 8: Installing other third-party packages, The well-maintained test and PyPI JSON API
Read classifiers as a floor, not a verdict. Many healthy packages update them only with their next release:
django-extensions 4.1 stops at 5.2 and django-storages 1.14.6 at 5.1, yet both installed and passed manage.py check
under Django 6.1.1 in the scratch project. A missing classifier is a reason to run your own test suite against the
package and to look at its CI and open issues, not by itself a reason to reject it. Conversely, a fresh release with no
docs and one maintainer is a risk even if it lists every Django version. When the answer to one of the questions is
"no", the tutorial’s suggestion is to treat it as an opportunity to contribute.
A curated ecosystem table
The Django project maintains a short, curated list of "incredibly useful and well-supported" community packages on its
ecosystem page, grouped by purpose. The table picks the eleven that a typical project such as Bookshelf meets first.
Versions, release dates and Django classifiers are from PyPI on 2026-10-03; "6.1 check" says whether the package was
installed into the Django 6.1.1 scratch project and passed manage.py check (Wagtail and django CMS are whole
applications with their own project templates and were not installed).
| Package (import name) | Ecosystem group | What it gives you | Latest on PyPI | Django classifiers | 6.1 check | Used on |
|---|---|---|---|---|---|---|
django-debug-toolbar ( |
Debugging & Development Tools |
Panels showing SQL queries, templates, cache calls, signals and settings for each request; development only. |
8.0.0 (2026-09-01) |
5.2, 6.0, 6.1 |
yes |
|
django-extensions ( |
Debugging & Development Tools |
Extra management commands ( |
4.1 (2025-04-11) |
4.2, 5.1, 5.2 |
yes |
|
django-allauth ( |
Authentication & Authorization |
Sign-up, login, email verification, password reset, MFA and social (OAuth/OpenID Connect) accounts. |
65.19.7 (2026-10-01) |
4.2-6.1 |
yes |
|
django-environ ( |
Environment Configuration |
Twelve-factor settings: typed environment variables, |
0.14.0 (2026-06-18) |
2.2-6.0 |
yes |
|
django-storages ( |
Storage & Static Files |
Storage backends for Amazon S3 and S3-compatible services, Azure, Google Cloud Storage, SFTP and others, configured
through |
1.14.6 (2025-04-02) |
3.2, 4.1-5.1 |
yes |
|
WhiteNoise ( |
Storage & Static Files |
Serves compressed, cache-busted static files straight from the Python process, without a separate web server. |
6.12.0 (2026-02-27) |
4.2-6.0 |
yes |
|
django-crispy-forms ( |
Forms & Views |
The |
2.7 (2026-07-29) |
5.2, 6.0, 6.1 |
yes |
|
django-filter ( |
Utilities & Miscellaneous |
Declarative |
26.1 (2026-07-11) |
5.2, 6.0, 6.1 |
yes |
|
django-import-export ( |
Admin Interface Enhancements |
Import and export model data (CSV, XLSX, JSON and more) from the admin, with previews and validation. |
4.4.1 (2026-05-05) |
4.2, 5.1, 5.2 |
yes |
|
Wagtail ( |
Content Management Systems |
A full CMS built on Django: page trees, StreamField content blocks, image and document libraries, editor workflows. |
8.0 (2026-08-25) |
5.2, 6.0, 6.1 |
not run |
— |
django CMS ( |
Content Management Systems |
An editor-friendly CMS with in-place editing, multilingual pages and a plugin architecture. |
5.1.3 (2026-09-23) |
5.2, 6.0, 6.1 |
not run |
— |
Source: Django community ecosystem: Community Packages and the PyPI pages listed in References
The scratch project installed the nine plain apps together and ran the check with all of them enabled:
from .settings import * # noqa: F403
INSTALLED_APPS += [ # noqa: F405
"django_extensions",
"import_export",
"storages",
"crispy_forms",
"crispy_bootstrap5",
"django_filters",
"allauth",
"allauth.account",
]
MIDDLEWARE += ["allauth.account.middleware.AccountMiddleware"] # noqa: F405
MIDDLEWARE.insert(1, "whitenoise.middleware.WhiteNoiseMiddleware") # noqa: F405
$ uv pip install django-extensions django-import-export whitenoise django-storages django-environ \
django-crispy-forms crispy-bootstrap5 django-filter django-allauth
$ python manage.py check --settings=config.eco_settings
System check identified no issues (0 silenced).
The WhiteNoise middleware goes directly after SecurityMiddleware (index 1 in the startproject list), and allauth 65.x
requires its AccountMiddleware; django-environ and django-debug-toolbar are configured as shown on their own pages. The
same ecosystem page lists further groups (API development with DRF and Django Ninja, background tasks, security
middleware, testing, internationalisation, email) and two "exploratory" packages that may move into Django itself; the
pages of this section link the relevant ones where they are used, and Django Packages covers the long tail.
Pitfalls checklist
-
Templates or static files missing from the wheel. With setuptools, list them in
MANIFEST.in(or use a VCS-aware plugin); always inspect the wheel withpython -m zipfile -lbefore publishing. -
Un-namespaced templates and static files (
templates/list.html,static/style.css) silently shadow, or are shadowed by, another app’s files. -
No
default_auto_fieldon theAppConfig: the app’s primary-key type then depends on each project’sDEFAULT_AUTO_FIELD, andmakemigrationsin a project creates migrations inside your installed package. -
A label that collides with a contrib app or another package, or a label changed after release.
-
Migrations generated from a real project, hard dependencies on
auth.Userinstead ofsettings.AUTH_USER_MODEL, or edited released migrations. -
Importing the project (
from config import settings,from bookshelf.models import …) from inside the app. -
Installing the
testspackage: exclude it from package discovery, or every app that forgets ends up fighting over a top-leveltestsmodule in users' environments. -
Classifiers that lie: claiming a Django version the test matrix does not run, or never updating them.
-
An upper bound on Django without a reason, which blocks users from security releases of the next feature version.
-
python setup.py sdist/bdist_wheel: runningsetup.pyas a command is deprecated; usepython -m buildoruv build. -
Treating "installed" as "integrated": most third-party packages also need URLs, middleware (in the right position) or settings; follow their installation guide, not a blog post written for an older version.
-
Development tools in production: django-debug-toolbar and
runserver_plusbelong in development settings and development dependencies only.
Where the books are outdated
Two Scoops of Django 1.11 (2017) has the most complete treatment of the subject among the four books: its chapter on
third-party packages recommends Django Packages, tells authors to declare install_requires in setup.py, scaffolds new
packages with cookiecutter-djangopackage, and builds with python setup.py sdist and python setup.py bdist_wheel
before uploading with twine, using a setup.cfg [wheel] universal = 1 section to produce Python 2/3 wheels. The advice
on choosing packages and on using twine instead of the old setup.py upload still holds. The mechanics do not:
-
[project]metadata inpyproject.toml(PEP 621, 2020) replacedsetup.pyarguments such asinstall_requires; Django’s tutorial shows onlypyproject.tomlsince the 5.1 docs (the 5.0 docs still usedsetup.cfgwith a stubsetup.py). -
Invoking
setup.pyas a command is deprecated;python -m buildanduv buildreplacesdist/bdist_wheel, andpip install .replacessetup.py install. -
Universal wheels are irrelevant now that every supported Django is Python 3 only; a pure-Python app builds a
py3-none-anywheel. -
Licences are declared with an SPDX expression (
license = "BSD-3-Clause") andlicense-files, not classifiers. -
PyPI trusted publishing lets CI upload without storing an API token.
The other books use many packages but do not package an app. Their package versions have moved on: crispy-forms 2
needs a separate template pack such as crispy-bootstrap5, model_mommy became model_bakery, and Django Debug
Toolbar’s setup now uses debug_toolbar_urls() instead of hand-written debug/ URL patterns. Check every package’s
current installation guide rather than copying a book’s settings.
What changed recently
| Version | Change relevant to this page |
|---|---|
Django 3.2 (2021-04-06) |
|
Django 4.1 (2022-08-03) |
|
Django 5.0 docs |
The advanced tutorial sets |
Django 5.1 docs |
The advanced tutorial uses |
Django 6.0 (2025-12-03) |
|
Django 6.1 (2026-09-02) |
Calendar versioning adopted: after 6.2 LTS come Django 2028 and Django 2029, and deprecation warnings are named
|
django-debug-toolbar 8.0.0 (2026-09-01) |
Current release, classified for Django 5.2-6.1; installation uses |
The Django rows come from the 3.2, 4.1, 6.0 and 6.1 release notes and from the advanced tutorial in each version of the docs; the package row from PyPI.