Working with Django Plugins: Building Reusable, Pip-Installable Apps

Blog / Django · May 4, 2016 · Updated June 10, 2026 · 8 min read
Working with Django Plugins: Building Reusable, Pip-Installable Apps

In Django, a "plugin" is really a reusable app: a self-contained Python package of models, views, templates, static files, and management commands that you list in INSTALLED_APPS and, when you want to share it, publish to PyPI as a pip install-able distribution. Django has no separate "plugin API" — the app is the plugin, and a modern pyproject.toml is all you need to ship it.

Key takeaways

  • A Django plugin = a reusable app added to INSTALLED_APPS; there is no special plugin runtime in core Django.
  • Package it with a pyproject.toml (setuptools or Hatchling) — the old setup.py-only flow and Python 2 are obsolete in 2026.
  • Use an AppConfig (in apps.py) for the app label, default field, signals, and startup hooks.
  • Ship templates and static files namespaced under app_name/ so they never clash with other apps.
  • For genuine pluggability, expose extension points with entry points ([project.entry-points]) and discover them via importlib.metadata.
  • Learn from battle-tested apps: django-allauth, djangorestframework, django-debug-toolbar, django-extensions, django-storages, django-filter, and Celery.

What is a Django "plugin", really?

Django ships no plugin manager, no register_plugin() call, and no hook bus in core. What people loosely call a "plugin" is just a reusable app — the same unit you create with startapp, but structured so it can live in any project. If you have ever run pip install djangorestframework and added 'rest_framework' to INSTALLED_APPS, you have already used a Django plugin.

Three things make an app genuinely reusable:

  1. No hard-coded project assumptions — it reads settings instead of importing your project's modules.
  2. Self-contained assets — its own templates, static files, migrations, and management commands.
  3. A distribution — packaging metadata so pip can install it from PyPI or a Git URL.

If you are new to building apps at all, start with our walkthrough on creating a Django app, then come back here to make it reusable.

In-project app vs reusable (pluggable) app

Aspect In-project app Reusable / pluggable app
Lives in One repo, one project Its own package, many projects
Install Already on the path pip install your-app
Config Imports project settings freely Reads settings, ships sane defaults
Templates / static Project-wide folders are fine Namespaced under app_name/
Packaging None needed pyproject.toml + build backend
Versioning Tied to the project SemVer with its own changelog
Distribution n/a PyPI, private index, or Git URL

The practical rule: build it as an in-project app first, and only extract it into a package once a second project actually needs it.

How do you structure a reusable Django app?

A distributable app keeps everything it needs inside one importable package and namespaces its assets so nothing collides with the host project. The modern, recommended shape uses a src/ layout:

django-awesome/
|-- pyproject.toml
|-- README.md
|-- LICENSE
`-- src/
    `-- awesome/
        |-- __init__.py
        |-- apps.py
        |-- models.py
        |-- views.py
        |-- urls.py
        |-- admin.py
        |-- migrations/
        |   `-- __init__.py
        |-- management/
        |   `-- commands/
        |       `-- sync_awesome.py
        |-- templatetags/
        |   |-- __init__.py
        |   `-- awesome_tags.py
        |-- templates/
        |   `-- awesome/
        |       `-- widget.html
        `-- static/
            `-- awesome/
                `-- awesome.css

Declare an AppConfig

Every reusable app should define an explicit AppConfig in apps.py. It sets the app label, the default auto field, and gives you a ready() hook for connecting signals — all without forcing the host project to know your internals. Since Django 3.2 you no longer set default_app_config; Django auto-discovers a single AppConfig, so installers just add "awesome" to INSTALLED_APPS.

# src/awesome/apps.py
from django.apps import AppConfig


class AwesomeConfig(AppConfig):
    name = "awesome"
    verbose_name = "Awesome"
    default_auto_field = "django.db.models.BigAutoField"

    def ready(self):
        # Import signal handlers, register system checks, etc.
        from . import signals  # noqa: F401

Namespace templates, static files, and tags

Django's loaders search every installed app's templates/ and static/ directories, so two apps that both ship widget.html will shadow each other. Always nest assets one level deeper under the app name — templates/awesome/widget.html and static/awesome/awesome.css — then reference them as {% include "awesome/widget.html" %} and {% static "awesome/awesome.css" %}.

Apply the same discipline to template tags: keep them in awesome/templatetags/, load them with {% load awesome_tags %}, and follow our guide to custom template tags and filters when you build them.

Ship management commands

Reusable apps often need a setup or sync step. Add management/commands/<name>.py and Django exposes it as python manage.py <name> for any project that installs your app. This is exactly how a sync_awesome command cleanly replaces the old, framework-specific "sync plugins" step. See our worked example of writing a custom management command for the full pattern.

# src/awesome/management/commands/sync_awesome.py
from django.core.management.base import BaseCommand

from awesome.models import Widget


class Command(BaseCommand):
    help = "Synchronise Awesome widgets defined in code with the database."

    def handle(self, *args, **options):
        created = Widget.sync_from_registry()
        self.stdout.write(self.style.SUCCESS(f"Synced {created} widgets"))

How do you package a Django app with pyproject.toml?

In 2026 the standard is a single pyproject.toml. A bare setup.py is legacy, and Python 2 is long dead — target Python 3.10+ and Django 4.2 LTS or 5.x. Pick any PEP 517 build backend; Hatchling and setuptools are the two most common. Here is a complete Hatchling example:

# pyproject.toml
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "django-awesome"
version = "1.0.0"
description = "A reusable Awesome app for Django."
readme = "README.md"
requires-python = ">=3.10"
license = "MIT"
authors = [{ name = "Your Team", email = "dev@example.com" }]
dependencies = ["Django>=4.2"]
classifiers = [
    "Framework :: Django",
    "Framework :: Django :: 5.0",
    "Programming Language :: Python :: 3",
]

[project.urls]
Homepage = "https://github.com/your-org/django-awesome"

[tool.hatch.build.targets.wheel]
packages = ["src/awesome"]

Include templates and static files in the build

Non-Python files do not ship by default. With Hatchling and a src/ layout, files inside the package directory are bundled automatically. With setuptools, set include_package_data = true and list the assets in a MANIFEST.in:

# MANIFEST.in (setuptools only)
recursive-include src/awesome/templates *
recursive-include src/awesome/static *
include LICENSE README.md

Build and publish

Build a wheel plus a source distribution, then upload to PyPI (or a private index) with Twine:

$ python -m pip install build twine
$ python -m build            # creates dist/*.whl and dist/*.tar.gz
$ python -m twine upload dist/*

Anyone can now install your app from PyPI, a private index, or straight from a Git repo:

$ pip install django-awesome
# or pin it straight from a repo:
$ pip install "git+https://github.com/your-org/django-awesome.git@v1.0.0"

Then add the app to INSTALLED_APPS and run migrations:

# settings.py
INSTALLED_APPS = [
    # ...
    "awesome",
]

How do you build real plugin discovery (entry points)?

When you want third-party packages to extend your app at runtime — the classic "plugin" use case — reach for Python entry points rather than a bespoke framework. Each plugin package advertises a callable under a named group in its own pyproject.toml, and your app discovers them at startup with importlib.metadata.entry_points().

First, a plugin package registers itself:

# A third-party plugin's pyproject.toml
[project.entry-points."awesome.plugins"]
pdf_export = "awesome_pdf.exporters:PDFExporter"
# src/awesome/registry.py
from importlib.metadata import entry_points


def load_plugins():
    """Discover every package that registered an 'awesome.plugins' entry point."""
    plugins = {}
    for ep in entry_points(group="awesome.plugins"):
        plugins[ep.name] = ep.load()  # import the class/callable
    return plugins

This is the same mechanism that powers pytest plugins, Flask extensions, and much of Django's ecosystem. It needs no extra dependency — importlib.metadata is in the standard library, and the entry_points(group=...) selection API shown here is stable on Python 3.10+.

Historically, third-party libraries such as django-plugins offered admin-managed plugin points backed by a database table (PluginPoint/Plugin models and a sync plugins command). In 2026 the same goals — registration, enable/disable, discovery — are met by reusable apps plus entry points with no extra dependency, which is why the reusable-app pattern is the recommended path.

Notable reusable Django apps to learn from

The fastest way to internalise good packaging is to read apps that already do it well:

Package What it adds Pattern worth copying
djangorestframework REST APIs Settings namespacing, app-level defaults
django-allauth Auth & social login Pluggable providers via app config
django-debug-toolbar Dev profiling panels Registry of swappable panels
django-extensions Extra mgmt commands management/commands/ at scale
django-storages File storage backends Swappable backend classes
django-filter Queryset filtering Reusable, composable form classes
Celery (django) Background tasks Task autodiscovery across apps

Open their pyproject.toml and apps.py side by side and you will see every convention above applied in production.

Need a hand?

Packaging, versioning, and CI for a shared Django app get fiddly once several teams depend on it. If you want help designing a reusable-app architecture or extracting tangled code into clean, installable packages, our Django development services team has shipped reusable apps and pluggable platforms since 2014, across 50+ projects for startups and enterprises.

Frequently Asked Questions

Is there an official Django plugin system?

No. Django core has no plugin manager or hook registry; the official unit of reuse is the app. You install a reusable app with pip, list it in INSTALLED_APPS, and it contributes models, views, templates, and commands. For runtime extensibility you layer Python entry points on top — there is nothing Django-specific to learn.

Do I still need setup.py to package a Django app?

No. A single pyproject.toml is the modern standard, read by both setuptools and Hatchling; you only keep a setup.py or setup.cfg for legacy edge cases. Target Python 3.10+ and Django 4.2 LTS or 5.x — any Python 2 packaging advice you find online is obsolete.

How do I make sure templates and static files are included in my package?

With Hatchling and a src/ layout, assets inside the package are bundled automatically. With setuptools, set include_package_data = true and list the folders in a MANIFEST.in. Always namespace them under templates/<app>/ and static/<app>/ so they do not collide with other installed apps.

What is the difference between django-plugins and a reusable app?

django-plugins was a third-party library that added admin-managed plugin points backed by database rows, popular in the Django 1.x era. Today the same goals — registration, enable/disable, discovery — are met with standard reusable apps plus importlib.metadata entry points, with no extra dependency and full Django 5.x support.

How do other packages extend my app at runtime?

Publish a named entry-point group (for example awesome.plugins) and document the interface plugins must implement. Third-party packages declare their plugin under that group in their own pyproject.toml, and your app loads them with entry_points(group="awesome.plugins"). This is exactly how pytest, Flask, and many Django tools handle extensions.

How should I version and distribute a reusable Django app?

Use semantic versioning, keep a changelog, and pin a supported Django range in dependencies (for example Django>=4.2). Distribute via PyPI for public apps, a private index such as self-hosted devpi or a cloud artifact registry for internal ones, or a pinned Git URL for quick sharing. Tag releases so installs stay reproducible.

Share this article