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. Reasons come from the configured backend; if that’s RemoteBackend, read the notes under RemoteBackend first.

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 come from the configured backend, same as error pages above. 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},
}

Things to consider before using it:

  • Every reason is an HTTP request made while the response is built, so a slow API slows down your pages by up to timeout seconds.

  • The default public endpoint is rate limited to 120 requests per minute. Past that it answers 429, and RemoteBackend falls back to local reasons for cooldown seconds. Fine for a hobby site; for anything busier, run your own no-as-a-service instance and point url at it, or stick with LocalBackend.

  • Error responses tend to come in floods, 429s especially. With error pages or the REST framework handler enabled, every one of them is also a request to url. Make sure your endpoint can take it.

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.1 (2026-09-25)

  • Fix RemoteBackend always falling back to local reasons: send a User-Agent header, since Cloudflare blocks urllib’s default with a 403

  • Error pages and the REST framework handler now use the configured backend instead of always using local reasons

  • Document RemoteBackend trade-offs and the default endpoint’s rate limit

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.1

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.1
File Size Uploaded
django_no-1.1.1.tar.gz 33.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for django-no 1.1.1
File Interpreter ABI Platform
django_no-1.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 67.6 kB

Release files / django_no-1.1.1.tar.gz

Download URL django_no-1.1.1.tar.gz
Size 33.7 kB
Tags Source
SHA-256 checksum
How to use checksums
a1246da9da7127ac801c16d75185dc3bd980fd7ddcfd417cbc827e34aa539d3c
BLAKE2b-256 checksum
How to use checksums
935d70d86724f1354e454d3d12575b46347fac411d6449f88bbdb15afb4c0b11
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.1-py3-none-any.whl

Download URL django_no-1.1.1-py3-none-any.whl
Size 34.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
59e03b07f7867576e0c1a85790461b7470c6723a4ca6caeb961da2af8dea7715
BLAKE2b-256 checksum
How to use checksums
acd87e9a987d2800170b3d8bffd99fa7b271e9a82bed8630c53ec33be31309a3
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

This release

1.1.1 This release

2 release files

1.1.0

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