Skip to main content

django-no

https://badge.fury.io/py/django-no.svg https://img.shields.io/pypi/pyversions/django-no.svg https://img.shields.io/pypi/djversions/django-no.svg https://github.com/bashu/django-no/actions/workflows/test.yml/badge.svg

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)

Source distribution for django-no 1.1.0
File Size Uploaded
django_no-1.1.0.tar.gz 33.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for django-no 1.1.0
File Interpreter ABI Platform
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 log

Release 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

Release history Release notifications | RSS feed

1.1.1

2 release files

This release

1.1.0 This release

2 release files

1.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page