django-no
Reusable django app that gives you a reason to say no. Reasons come from a bundled list or from a remote No-as-a-Service (NaaS) API, and can be rendered in templates or fetched from Python code.
Authored by Basil Shubin, and some great contributors.
Installation
First install the module, preferably in a virtual environment. It can be installed from PyPI:
pip install django-no
Requires Python 3.10+ and Django 5.2+.
Setup
You’ll need to add no to INSTALLED_APPS in your project’s settings.py file:
INSTALLED_APPS += [
"no",
]
There are no models, so no migrations are needed.
Usage
Load the no template tag library and use the {% no %} tag:
{% load no %}
<blockquote>{% no %}</blockquote>
Or store the reason in a variable:
{% load no %}
{% no as reason %}
<p title="{{ reason }}">No.</p>
Every {% no %} picks a new reason. Reasons are auto-escaped like any other template output.
From Python code:
from no import get_reason
get_reason() # "I'm on a strict 'no commitments' diet."
Or from the command line:
./manage.py no
Error pages
Tell users why not on 403 pages, in your root URLconf:
handler403 = "no.views.permission_denied"
Django has no handler429, so hand no.views.too_many_requests to your rate limiter instead, e.g. django-ratelimit:
RATELIMIT_VIEW = "no.views.too_many_requests"
Both render your own 403.html / 429.html if you have one, with {{ reason }} and {{ exception }} in the context, and fall back to a minimal built-in page otherwise. Like the REST framework handler below, they only use local reasons.
Django REST framework
Add a reason to every 403 and 429 JSON response:
pip install django-no[drf]
REST_FRAMEWORK = {
"EXCEPTION_HANDLER": "no.contrib.rest_framework.exception_handler",
}
{
"detail": "You do not have permission to perform this action.",
"reason": "I'm on a strict 'no commitments' diet."
}
Reasons always come from the local list here, never from a remote API, so a flood of throttled requests doesn’t turn into a flood of outgoing ones. Already have a custom exception handler? Pass its response through no.contrib.rest_framework.add_reason().
Configuration
Everything is optional. Without any configuration reasons are picked at random from the bundled list. To change that, set NO in your settings.py:
NO = {
"BACKEND": "no.backends.local.LocalBackend",
"OPTIONS": {},
}
BACKEND is the dotted path to a backend class and OPTIONS is passed to it as keyword arguments.
LocalBackend
The default. Picks a random reason from a JSON file containing a list of strings.
- path
Path to your own reasons file. Defaults to the bundled reasons.json.
NO = {
"BACKEND": "no.backends.local.LocalBackend",
"OPTIONS": {"path": BASE_DIR / "reasons.json"},
}
RemoteBackend
Fetches a reason from an HTTP API that responds with {"reason": "..."}. Whenever the request fails it falls back to LocalBackend and stops calling the API for cooldown seconds.
- url
API endpoint, must be http or https. Defaults to https://naas.isalman.dev/no.
- timeout
Request timeout in seconds. Defaults to 3.
- cooldown
Seconds to wait before retrying the API after a failure. Defaults to 60.
- path
Reasons file for the fallback LocalBackend.
NO = {
"BACKEND": "no.backends.remote.RemoteBackend",
"OPTIONS": {"timeout": 1, "cooldown": 300},
}
Keep in mind the request is made while the template renders, so a slow API slows down your page by up to timeout seconds.
Custom backends
Subclass no.backends.base.BaseBackend and implement get_reason(). OPTIONS are available as self.options:
from no.backends.base import BaseBackend
class PoliteBackend(BaseBackend):
def get_reason(self):
return self.options.get("reason", "No, thank you.")
Example
Please see the example application. This application is used to manually test the functionalities of this package. This also serves as a good example.
Contributing
If you’ve found a bug, implemented a feature or have a good reason to say no and think it is useful then please consider contributing. Patches, pull requests or just suggestions are welcome!
License
django-no is released under the MIT license.
Changes
1.1.0 (2026-09-25)
Add no management command that prints a reason to say no
Add Django REST framework exception handler that adds a reason to 403 and 429 responses (pip install django-no[drf])
Add permission_denied (handler403) and too_many_requests error views with built-in fallback templates
1.0.0 (2026-09-25)
Initial release
Release files for django-no 1.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| django_no-1.1.0.tar.gz | 33.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| django_no-1.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 67.1 kB
Release files / django_no-1.1.0.tar.gz
| Download URL | django_no-1.1.0.tar.gz |
|---|---|
| Size | 33.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
f6b351e45368698948d13af99670c5b3dba26dcb4957ad59d6e93d4af2d81652
|
|
BLAKE2b-256 checksum How to use checksums |
c054e66d9258f0ffc6661cd8e3a237ca2d17955667cf292be874d03514ea53d9
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 25, 2026.
Transparency logRelease files / django_no-1.1.0-py3-none-any.whl
| Download URL | django_no-1.1.0-py3-none-any.whl |
|---|---|
| Size | 33.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
61f2f65c5674c417a2af5cc405cd31ecc2a61298decf4b9c431d0490fb1c1d1d
|
|
BLAKE2b-256 checksum How to use checksums |
c8e58640e8ec6c285616269049d7038f4b954d5b8fa0906be7f8b1307c5afc83
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 25, 2026.
Transparency log