In the Django framework, a project refers to the collection of configuration files and code for a particular website. Django groups business logic into what it calls apps, which are the modules of the Django framework. There’s plenty of documentation on how to structure your projects and the apps within them, but when it comes time to package an installable Django app, information is harder to find.
In this tutorial, you’ll learn how to take an app out of a Django project and package it so that it’s installable. Once you’ve packaged your app, you can share it on PyPI so that others can fetch it through pip
.
In this tutorial, you’ll learn:
- What the differences are between writing stand-alone apps and writing apps inside of projects
- How to create a
pyproject.toml
file for publishing your Django app - How to bootstrap Django outside of a Django project so you can test your app
- How to test across multiple versions of Python and Django using
nox
- How to publish your installable Django app to PyPI using Twine
This tutorial includes a working package to help guide you through the process of making an installable Django app. You can download the source code by clicking the link below:
Get Your Code: Click here to download the free sample code that shows you how to write an installable Django app.
Prerequisites
This tutorial requires some familiarity with Django, pip
, PyPI, pyenv
—or an equivalent virtual environment tool—and nox
. To learn more about these, you can check out the following resources:
Starting a Sample Django App in a Project
Even if you set out to make your Django app available as a package, you’ll likely start inside a project. In the sample code, you’ll find a 000_before
directory that shows the code before the app is moved onto its own, demonstrating the process of transitioning from a Django project to an installable Django app.
You can also download the finished app at the PyPI realpython-django-receipts package page, or install the package by running python -m pip install realpython-django-receipts
.
The sample app is a short representation of the line items on a receipt. In the 000_before
folder, you’ll find a directory named sample_project
that contains a working Django project:
sample_project/
│
├── receipts/
│ ├── fixtures/
│ │ └── receipts.json
│ │
│ ├── migrations/
│ │ ├── 0001_initial.py
│ │ └── __init__.py
│ │
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── models.py
│ ├── tests.py
│ ├── urls.py
│ └── views.py
│
├── sample_project/
│ ├── __init__.py
│ ├── asgi.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
│
├── db.sqlite3
├── manage.py
├── resetdb.sh
└── runserver.sh
This tutorial was written using Django 5.0.7 and it was tested with Python 3.8 through 3.12. All of the steps outlined in this tutorial should be compatible with earlier versions of Django going back to Django 1.8. However, some modifications will be necessary if you’re using Python 2. For simplicity, the examples in this tutorial assume at least Python 3.8 across the code base.
Creating the Django Project From Scratch
The sample project and receipts app were created using the Django admin
command and some small edits. To start, run the following code inside of a clean virtual environment:
$ python -m pip install Django
$ django-admin startproject sample_project
$ cd sample_project
$ python manage.py startapp receipts
This creates the sample_project
project directory structure and a receipts
app subdirectory with template files that you’ll use to create your installable Django app.
Next, the sample_project/settings.py
file needs a few modifications:
- Add
"127.0.0.1"
to theALLOWED_HOSTS
setting so you can test locally. - Add
"receipts"
to theINSTALLED_APPS
list.
You’ll also need to register the receipts
app’s URLs in the sample_project/urls.py
file. To do so, add path("receipts/", include("receipts.urls"))
to the url_patterns
list. Note that you’ll need to add the include
function as an import from django.urls
.
Exploring the Receipts Sample App
The app consists of two ORM model classes: Item
and Receipt
. The Item
class contains database field declarations for a description and a cost. The cost is contained in a DecimalField
. It’s never a good idea to use floating-point numbers to represent money. Instead, you should always use fixed-point numbers when dealing with currencies.
The Receipt
class is a collection point for Item
objects. This is achieved with a ForeignKey
on Item
that points to Receipt
. Receipt
also includes total()
for getting the total cost of Item
objects contained in the Receipt
:
receipts/models.py
from decimal import Decimal
from django.db import models
class Receipt(models.Model):
created = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"Receipt(id={self.id})"
def total(self) -> Decimal:
return sum(item.cost for item in self.item_set.all())
class Item(models.Model):
created = models.DateTimeField(auto_now_add=True)
description = models.TextField()
cost = models.DecimalField(max_digits=7, decimal_places=2)
receipt = models.ForeignKey(Receipt, on_delete=models.CASCADE)
def __str__(self):
return (
f"Item(id={self.id}, description={self.description}, "
f"cost={self.cost})"
)
The model objects give you content for the database. A short Django view returns a JSON dictionary with all the Receipt
objects and their Item
objects in the database:
receipts/views.py
from django.http import JsonResponse
from receipts.models import Receipt
def receipt_json(request):
results = {
"receipts": [],
}
for receipt in Receipt.objects.all():
line = [str(receipt), []]
for item in receipt.item_set.all():
line[1].append(str(item))
results["receipts"].append(line)
return JsonResponse(results)
In this example, the receipt_json()
view iterates over all the Receipt
objects, creating a pair of the Receipt
objects and a list of the Item
objects contained within. All of this is put in a dictionary and returned through Django’s JsonResponse()
.
To make the models available in the Django admin interface, you use an admin.py
file to register the models as shown below:
receipts/admin.py
from django.contrib import admin
from receipts.models import Receipt, Item
@admin.register(Receipt)
class ReceiptAdmin(admin.ModelAdmin):
pass
@admin.register(Item)
class ItemAdmin(admin.ModelAdmin):
pass
This code creates a Django ModelAdmin
for each of the Receipt
and Item
classes and registers them with the Django admin.
Finally, a urls.py
file registers a single view in the app against a URL:
receipts/urls.py
from django.urls import path
from receipts import views
urlpatterns = [
path("receipt_json/", views.receipt_json),
]
You can now include receipts/urls.py
in your project’s url.py
file to make the receipt view available on your website.
With everything in place, you can run python manage.py makemigrations receipts
to add the necessary tables to the database. Then, you can go ahead and use the Django admin to add data. Note that you’ll need to create a superuser first, and you can do this with python manage.py createsuperuser
. Once you have some data, you can visit receipts/receipt_json/
to view the results:
$ curl -sS http://127.0.0.1:8000/receipts/receipt_json/ | python -m json.tool
{
"receipts": [
[
"Receipt(id=1)",
[
"Item(id=1, description=wine, cost=15.25)",
"Item(id=2, description=pasta, cost=22.30)"
]
],
[
"Receipt(id=2)",
[
"Item(id=3, description=beer, cost=8.50)",
"Item(id=4, description=pizza, cost=12.80)"
]
]
]
}
In this example, you use curl
to visit the receipt_json
view, which results in a JSON response containing the Receipt
objects and their corresponding Item
objects. You use json.tool
to format the output.
Testing the App in the Project
Django augments the Python unittest
package with its own testing capabilities, enabling you to preload fixtures into the database and run your tests. The receipts app defines a tests.py
file and a fixture for testing. This test is by no means comprehensive, but it’s a good enough proof of concept:
receipts/tests.py
from decimal import Decimal
from django.test import TestCase
from receipts.models import Receipt
class ReceiptTest(TestCase):
fixtures = ["receipts.json", ]
def test_receipt(self):
receipt = Receipt.objects.get(id=1)
total = receipt.total()
expected = Decimal("37.55")
self.assertEqual(expected, total)
The fixture creates two Receipt
objects and four corresponding Item
objects. Click on the collapsible section below for a closer look at the code for the fixture.
A Django test fixture is a serialization of the objects in the database. The following JSON code creates Receipt
and Item
objects for testing:
[
{
"model": "receipts.receipt",
"pk": 1,
"fields": {
"created": "2024-07-18T14:23:39.083Z"
}
},
{
"model": "receipts.receipt",
"pk": 2,
"fields": {
"created": "2024-07-18T14:23:41.330Z"
}
},
{
"model": "receipts.item",
"pk": 1,
"fields": {
"created": "2024-07-18T14:23:54.117Z",
"description": "wine",
"cost": "15.25",
"receipt": 1
}
},
{
"model": "receipts.item",
"pk": 2,
"fields": {
"created": "2024-07-18T14:24:02.095Z",
"description": "pasta",
"cost": "22.30",
"receipt": 1
}
},
{
"model": "receipts.item",
"pk": 3,
"fields": {
"created": "2024-07-18T14:24:12.284Z",
"description": "beer",
"cost": "8.50",
"receipt": 2
}
},
{
"model": "receipts.item",
"pk": 4,
"fields": {
"created": "2024-07-18T14:24:20.802Z",
"description": "pizza",
"cost": "12.80",
"receipt": 2
}
}
]
The above fixture is referenced in the ReceiptTestCase
class and is loaded automatically by the Django test harness.
You can test the receipts app with the Django manage.py
command:
$ python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 1 test in 0.013s
OK
Destroying test database for alias 'default'...
Running python manage.py test
runs the single test defined in receipts/tests.py
and displays the results.
Making Your Installable Django App
Your goal is to share the receipts app without a project and make it reusable by others. You could zip up the receipts/
directory and hand it out, but that’s somewhat limiting. Instead, you want to separate the app into a package so it’s installable.
The biggest challenge in creating an installable Django app is that Django expects a project. An app without a project is just a directory containing code. Without a project, Django doesn’t know how to do anything with your code, including running tests.
Moving Your Django App Out of the Project
It’s a good idea to keep a sample project around so you can run the Django dev server and play with a live version of your app. You won’t include this sample project in the app package, but it can still live in your repository.
Additionally, it’s now common practice when packaging code to put it in a src
directory. Following this idea, you can get started with packaging your installable Django app and create a src
directory as a sibling of the sample project. Then, you can move the receipts
directory into it:
$ mkdir src
$ mv sample_project/receipts src
The directory structure now looks something like this:
django-receipts/
│
├── sample_project/
│ ├── sample_project/
│ │ ├── __init__.py
│ │ ├── asgi.py
│ │ ├── settings.py
│ │ ├── urls.py
│ │ └── wsgi.py
│ │
│ ├── db.sqlite3
│ ├── manage.py
│ ├── resetdb.sh
│ └── runserver.sh
│
├── LICENSE
├── README.rst
└── src
└── receipts
├── __init__.py
├── admin.py
├── apps.py
├── fixtures
│ └── receipts.json
├── migrations
│ ├── 0001_initial.py
│ └── __init__.py
├── models.py
├── tests.py
├── urls.py
└── views.py
To package your app, you need to pull it out of the project. Moving it is the first step. It’s a good idea to keep the original project for testing purposes, but you shouldn’t include it in the resulting package.
Bootstrapping Django Outside of a Project
Now that your app is outside of the sample Django project, you need to tell Django how to find it. If you want to test your app, run a Django shell that can find your app, or run your migrations. First, you’ll need to configure Django and make it available.
Django’s settings.configure()
and django.setup()
are key to interacting with your app outside of a project. More information on these calls is available in the Django documentation.
You’re likely to need this configuration of Django in several places, so it makes sense to define it in a function. Create a file called boot_django.py
that contains the following code:
boot_django.py
1# File sets up the django environment, used by other scripts that need to
2# execute in Django land
3import sys
4from pathlib import Path
5import django
6from django.conf import settings
7
8BASE_DIR = Path(__file__).parent / "src"
9sys.path.insert(0, str(BASE_DIR))
10
11def boot_django():
12 settings.configure(
13 BASE_DIR=BASE_DIR,
14 DEBUG=True,
15 DATABASES={
16 "default":{
17 "ENGINE":"django.db.backends.sqlite3",
18 "NAME": BASE_DIR / "db.sqlite3",
19 }
20 },
21 INSTALLED_APPS=(
22 "receipts",
23 ),
24 TIME_ZONE="UTC",
25 USE_TZ=True,
26 )
27 django.setup()
In this code block, Line 8 defines the location of the src
directory, and line 9 adds it to Python’s path so that the interpreter can load it as a module.
Lines 12 and 27 set up the Django environment. The settings.configure()
call takes a list of arguments that are equivalent to the variables defined in a settings.py
file. Anything you would need in your settings.py
to make your app run gets passed into settings.configure()
.
The above code is a fairly stripped-down configuration. The receipts app doesn’t do anything with sessions or templates, so INSTALLED_APPS
only needs "receipts"
, and you can skip over any middleware definitions. The USE_TZ=True
value is necessary because the Receipt
model contains a created
timestamp. Otherwise, you would run into problems loading the test fixture.
Running Management Commands With Your Installable Django App
Now that you have boot_django.py
, you can run any Django management command with a very short script:
makemigrations.py
#!/usr/bin/env python
from django.core.management import call_command
from boot_django import boot_django
boot_django()
call_command("makemigrations", "receipts")
Django allows you to programmatically call management commands through call_command()
. You can now run any management command by importing and calling boot_django()
followed by call_command()
.
Your app is now outside the project, allowing you to do all sorts of Django-y things to it. Here are four utility scripts you can define:
load_tests.py
to test your appmakemigrations.py
to create migration filesmigrate.py
to perform table migrationsdjangoshell.py
to spawn a Django shell that’s aware of your app
Testing Your Installable Django App
The load_tests.py
file could be as simple as the makemigrations.py
script, but then it would only be able to run all the tests at once. With a few additional lines, you can pass command-line arguments to the test runner, which allows you to run selective tests:
load_tests.py
1#!/usr/bin/env python
2import sys
3from unittest import TestSuite
4from boot_django import boot_django
5
6boot_django()
7
8default_labels = ["receipts.tests", ]
9
10def get_suite(labels=default_labels):
11 from django.test.runner import DiscoverRunner
12 runner = DiscoverRunner(verbosity=1)
13 failures = runner.run_tests(labels)
14 if failures:
15 sys.exit(failures)
16
17 # In case this is called from setuptools, return a test suite
18 return TestSuite()
19
20if __name__ == "__main__":
21 command_line_labels = sys.argv[1:]
22 labels = command_line_labels if command_line_labels else default_labels
23 get_suite(labels)
Django’s DiscoverRunner
is a test discovery class compatible with Python’s unittest
. It’s responsible for setting up the test environment, building the suite of tests, setting up the databases, running the tests, and then tearing it all down. Starting on line 10, get_suite()
takes a list of test labels and directly calls the DiscoverRunner
on them.
This script is similar to what the Django management command test
does. The name-main block passes any command-line arguments to get_suite()
, and if there are none, then it passes in the test suite for the app, receipts.tests
. You can now call load_tests.py
with a test label argument and run a single test.
Line 18 is a special case to help when testing with certain external tools. For example, tox
expects a TestSuite
object even though line 13 is where the testing actually happens. This isn’t necessary with nox
, the multi-version testing tool you’ll learn more about in a later section. You can also check out a potential substitute for DiscoverRunner
in the collapsible section below.
One of the installable Django apps that I’ve written is django-awl. It’s a loose collection of utilities that I’ve accumulated over my years writing Django projects. Included in the package is an alternative to DiscoverRunner
called WRunner
.
The key advantage to using WRunner
is that it supports wildcard matching of test labels. Passing in a label that begins with an equals sign (=
) will match any test suite or method name that contains that label as a substring. For example, the label =rec
would match and run the test ReceiptTest.test_receipt()
in receipt/tests.py
.
Defining Your Installable Package With pyproject.toml
To put your installable Django app on PyPI, you need to first put it in a package. PyPI expects a wheel
or source distribution. A wheel
gets built using build
. To do this, you need to create a pyproject.toml
at the same directory level as your src
directory.
Before digging into that, you’ll want to make sure you have some documentation. You can include a project description in pyproject.toml
, which is automatically displayed on the PyPI project page. Make sure to write a README
file README.rst
with information about your package.
PyPI supports the reStructuredText format by default, but it can also handle Markdown with extra parameters:
pyproject.toml
1[build-system]
2requires = ["setuptools >= 40.9.0", "wheel"]
3build-backend = "setuptools.build_meta"
4
5[project]
6name = "realpython-django-receipts"
7version = "1.1.0"
8description = "Sample installable django app"
9readme = "README.rst"
10license = {file = "LICENSE"}
11classifiers = [
12 "Development Status :: 4 - Beta",
13 "Environment :: Web Environment",
14 "Intended Audience :: Developers",
15 "License :: OSI Approved :: MIT License",
16 "Operating System :: OS Independent",
17 "Programming Language :: Python :: 3 :: Only",
18 "Programming Language :: Python :: 3.8",
19 "Programming Language :: Python :: 3.9",
20 "Programming Language :: Python :: 3.10",
21 "Programming Language :: Python :: 3.11",
22 "Programming Language :: Python :: 3.12",
23 "Programming Language :: Python :: Implementation :: CPython",
24 "Topic :: Software Development :: Libraries :: Application Frameworks",
25 "Topic :: Software Development :: Libraries :: Python Modules",
26]
27requires-python = ">=3.8"
28dependencies = [
29 "Django>=4.2",
30]
31
32[project.urls]
33Repository = "https://github.com/realpython/django-receipts"
34
35[project.optional-dependencies]
36dev = [
37 "build==1.2.1",
38 "nox==2024.4.15",
39 "twine==5.1.1",
40]
This pyproject.toml
file describes the package that you’ll build. The Python packaging ecosystem has changed rapidly in the last few years, and work is still ongoing. By using setuptools
40.9.0 or greater, the amount of configuration is simplified and you’ll typically only need the pyproject.toml
file.
Lines 1 to 3 declare which build back end to use, enforcing an up-to-date version of setuptools
. The [project]
section of pyproject.toml
includes the metadata about your project.
Lines 6 to 10 specify the project’s name, version, a short description, and the associated README.rst
and LICENSE
files.
Line 27 indicates a minimum version of acceptable Python, while lines 28 to 30 contain the project’s dependencies. Any installers, such as python -m pip install
, will know to also install the dependencies you’ve declared. Keep in mind that you always want to tie your installable Django app to its minimum supported version of Django.
If your code has dependencies that are only required to build the system or run tests, then you can add a [project.optional-dependencies]
section, as shown in lines 35 to 40. Any labels declared within this section can be optionally installed. The configuration shown here creates a dependency group called dev
, which can be installed with python -m pip install realpython-django-receipts[dev]
.
You’re almost ready to build the package for your installable Django app. The easiest way to test it is with your sample project—another good reason to keep the sample project around. The python -m pip install
command supports locally defined packages, which can be used to make sure your app still works with a project.
You can install a locally editable version of a package to test it using the -e
option to pip
. To ensure everything is working as intended, it’s best to start with a brand-new virtual environment. Add a new requirements.txt
file containing the following:
requirements.txt
-e ../../django-receipts[dev]
The -e
tells pip
that this is a local editable installation. You’re now ready to install:
$ python -m pip install -r requirements.txt
Obtaining django-receipts (from -r requirements.txt (line 1))
Installing build dependencies ... done
Checking if build backend supports build_editable ... done
Getting requirements to build editable ... done
Preparing editable metadata (pyproject.toml) ... done
Building wheels for collected packages: realpython-django-receipts
Building editable for realpython-django-receipts (pyproject.toml) ... done
Created wheel for realpython-django-receipts: filename=realpython_django_receipts-1.1.0-0.editable-py3-none-any.whl size=3205
Stored in directory: /private/var/folders/x0/pip-ephem-wheel-cache-dmncdc7r/wheels/52/66/07/
Successfully built realpython-django-receipts
Installing collected packages: realpython-django-receipts
Successfully installed realpython-django-receipts-1.1.0
The dependencies list in pyproject.toml
tells pip
that it needs Django. In the background, Django needs asgiref
, pytz
, and sqlparse
. All the dependencies are taken care of and you should now be able to run your sample_project
Django dev server. Congratulations—your app is now packaged and referenced from within the sample project!
Testing Multiple Versions With nox
Django and Python are both constantly advancing. If you’re going to share your installable Django app with the world, then you’ll need to test it in multiple environments. The third-party nox
library allows you to write short Python programs that create multiple virtual environments for all combinations of supported configurations.
You install nox
with pip -m install nox
, which adds a nox
command to your environment. When you run nox
, it looks for a file named noxfile.py
, which defines your testing configuration. Your noxfile.py
code is responsible for declaring all combinations of Python and the dependencies that you want to test:
noxfile.py
1import nox
2
3# 4.2 is LTS end of life April 2026
4@nox.session(python=["3.8", "3.9", "3.10", "3.11", "3.12"])
5def test420(session):
6 session.install("django>=4.2,<4.3")
7 session.run("./load_tests.py", external=True)
8
9# 5.0 of life April 2025
10@nox.session(python=["3.10", "3.11", "3.12"])
11def test500(session):
12 session.install("django>=5.0,<5.1")
13 session.run("./load_tests.py", external=True)
The above file contains two groupings of configurations, each of which gets its own function. The @nox.session()
decorator defines which versions of Python to use with each configuration. You use session.install()
inside a configuration function to install one or more packages, and session.run()
to define the actual test.
Line 6 says that test420()
should use a version of Django between 4.2 and 4.3. Line 7 calls the load_tests.py
script. The external=True
argument silences any warnings caused by load_tests.py
not being within the virtual environment.
When you run nox
, it evaluates noxfile.py
and creates a virtual environment for each configuration combination. The test420()
function results in five virtual environments, one for each version of Python from 3.8 through 3.12. The nox
command also allows you to run a subset of environments in case you need to hunt down a problem in one particular configuration. For more information, see the command-line usage section of the nox
documentation.
Publishing to PyPI
Finally, it’s time to share your installable Django app on PyPI. There are multiple tools for uploading a package, but in this tutorial you’ll use Twine. The following code builds the packages and invokes Twine:
$ python -m build
$ twine upload dist/*
The first command builds the source and binary distributions of your package, and the call to twine
uploads it to PyPI. Of course, you’ll need a PyPI account for it to work. If you have a .pypirc
file in your home directory, then you can preset your username so the only thing you’re prompted for is your password:
.pypirc
[disutils]
index-servers =
pypi
[pypi]
username: <YOUR_USERNAME>
You can use a small shell script to grep
the version number from the code. Then, call git tag
to tag the repo with the version number, remove the old dist/
directories, and call the above commands.
Note: For more details on how to use Twine, check out the How to Publish an Open-Source Python Package to PyPI tutorial. It’s also worth noting that there are two popular alternatives to Twine: Poetry and Flit.
Conclusion
Django apps rely on the Django project structure, so packaging them separately requires extra steps. You’ve seen how to make an installable Django app by extracting it from a project, packaging it, and sharing it on PyPI.
In this tutorial, you’ve learned how to:
- Use the Django framework outside of a project
- Call Django management commands on an app that’s independent of a project
- Write a script that invokes Django tests with an optional single test label
- Build a
pyproject.toml
file to define your package - Write a
noxfile.py
script to test multiple configurations - Use Twine to upload your installable Django app
You’re all set to share your next app with the world. Happy coding!
Get Your Code: Click here to download the free sample code that shows you how to write an installable Django app.
Further Reading
Django, packaging, and testing are all very deep topics. There’s lots of information out there. To dig in deeper, check out the following resources:
- Django Documentation
- Get Started With Django: Build a Portfolio App
- Django Tutorials
- Managing Multiple Python Versions With pyenv
- Using Python’s pip to Manage Your Projects’ Dependencies
- Getting Started With Testing in Python
- Poetry
- Flit
PyPI has loads of installable Django apps that are worth trying out. Here are some of the most popular: