Skip to main content

django-zeal

Catch N+1 queries in your Django project.

Static Badge PyPI - Version

🔥 Battle-tested at Cinder

Features

  • Detects N+1s from missing prefetches and from use of .defer()/.only()/.get()
  • Friendly error messages like N+1 detected on social.User.followers at myapp/views.py:25 in get_user
  • Configurable thresholds
  • Allow-list
  • Well-tested
  • No dependencies
  • Fast; adds very little overhead to your test suite

Acknowledgements

This library draws heavily from jmcarp's nplusone. It's not a fork, but a lot of the central concepts and initial code came from nplusone.

Installation

First:

uv add django-zeal       # if using uv
pip install django-zeal  # if using pip

Then, add zeal to your INSTALLED_APPS and MIDDLEWARE.

if DEBUG:
    INSTALLED_APPS.append("zeal")
    MIDDLEWARE.append("zeal.middleware.zeal_middleware")

This will detect N+1s that happen in web requests. To catch N+1s in more places, read on!

Celery

If you use Celery, you can configure this using signals:

from celery.signals import task_prerun, task_postrun
from zeal import setup, teardown
from django.conf import settings

@task_prerun.connect()
def setup_zeal(*args, **kwargs):
    setup()

@task_postrun.connect()
def teardown_zeal(*args, **kwargs):
    teardown()

Tests

Django runs tests with DEBUG=False, so to run zeal in your tests, you'll first need to ensure it's added to your INSTALLED_APPS and MIDDLEWARE. You could do something like:

import sys

TEST = "test" in sys.argv
if DEBUG or TEST:
    INSTALLED_APPS.append("zeal")
    MIDDLEWARE.append("zeal.middleware.zeal_middleware")

This will enable zeal in any tests that go through your middleware. If you want to enable it in all tests, you need to do a bit more work.

If you use pytest, use a fixture in your conftest.py:

import pytest
from zeal import zeal_context

@pytest.fixture(scope="function", autouse=True)
def use_zeal():
    with zeal_context():
        yield

If you use unittest, add custom test cases and inherit from these rather than directly from Django's test cases:

# In e.g. `myapp/testing/test_cases.py`
from zeal import setup as zeal_setup, teardown as zeal_teardown
import unittest
from django.test import SimpleTestCase, TestCase, TransactionTestCase

class ZealTestMixin(unittest.TestCase):
    def setUp(self, test):
        zeal_setup()
        super().setUp()

    def teardown(self) -> None:
        zeal_teardown()
        return super().teardown(test, err)

class CustomSimpleTestCase(ZealTestMixin, SimpleTestCase):
    pass

class CustomTestCase(ZealTestMixin, TestCase):
    pass

class CustomTransactionTestCase(ZealTestMixin, TransactionTestCase):
    pass

Generic setup

If you also want to detect N+1s in other places not covered here, you can use the setup and teardown functions, or the zeal_context context manager:

from zeal import setup, teardown, zeal_context


def foo():
    setup()
    try:
        # your code goes here
    finally:
        teardown()


@zeal_context()
def bar():
    # your code goes here


def baz():
    with zeal_context():
        # your code goes here

Configuration

By default, any issues detected by zeal will raise a ZealError. If you'd rather log any detected N+1s as warnings, you can set:

ZEAL_RAISE = False

N+1s will be reported when the same query is executed twice. To configure this threshold, set the following in your Django settings.

ZEAL_NPLUSONE_THRESHOLD = 3

To handle false positives, you can temporarily disable zeal in parts of your code using a context manager:

from zeal import zeal_ignore

with zeal_ignore():
    # code in this block will not log/raise zeal errors

If you only want to ignore a specific N+1, you can pass in a list of models/fields to ignore:

with zeal_ignore([{"model": "polls.Question", "field": "options"}]):
    # code in this block will ignore N+1s on Question.options

To detect unnecessary zeal_ignore() blocks, enable this opt-in check in your Django settings:

ZEAL_CHECK_UNUSED_IGNORES = True

This makes unnecessary zeal_ignores (that suppress no N+1s) raise zeal.UnusedIgnoreError. With ZEAL_RAISE = False, it emits a warning instead.

This can be useful for tidying up your codebase by removing redundant zeal_ignore blocks.

If you want to listen to N+1 exceptions globally and do something with them, you can listen to the Django signal that zeal emits:

from zeal.signals import nplusone_detected
from django.dispatch import receiver

@receiver(nplusone_detected)
def handle_nplusone(sender, exception):
    # do something

Finally, if you want to ignore N+1 alerts from a specific model/field globally, you can add it to your settings:

ZEAL_ALLOWLIST = [
    {"model": "polls.Question", "field": "options"},

    # you can use fnmatch syntax in the model/field, too
    {"model": "polls.*", "field": "options"},

    # if you don't pass in a field, all N+1s arising from the model will be ignored
    {"model": "polls.Question"},
]

Debugging N+1s

By default, zeal's alerts will tell you the line of your code that executed the same query multiple times. If you'd like to see the full call stack from each time the query was executed, you can set:

ZEAL_SHOW_ALL_CALLERS = True

in your settings. This will give you the full call stack from each time the query was executed.

Comparison to nplusone

zeal borrows heavily from nplusone, but has some differences:

  • zeal also detects N+1 caused by using .only() and .defer()
  • it lets you configure your own threshold for what constitutes an N+1
  • it has slightly more helpful error messages that tell you where the N+1 occurred
  • nplusone patches the Django ORM even in production when it's not enabled. zeal does not!
  • nplusone appears to be abandoned at this point.
  • however, zeal only works with Django, whereas nplusone can also be used with SQLAlchemy.
  • zeal does not (yet) detect unused prefetches, but nplusone does.

Metadata

Release files for django-zeal 2.3.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-zeal 2.3.0
File Size Uploaded
django_zeal-2.3.0.tar.gz 28.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for django-zeal 2.3.0
File Interpreter ABI Platform
django_zeal-2.3.0-py3-none-any.whl Python 3 none any Details

Total release size: 45.5 kB

Release files / django_zeal-2.3.0.tar.gz

Download URL django_zeal-2.3.0.tar.gz
Size 28.6 kB
Tags Source
SHA-256 checksum
How to use checksums
c68d0e673ca672ba718df8f091e30b73fe32cd3aa85df6e7a464b498496505ba
BLAKE2b-256 checksum
How to use checksums
ed1a4ef8fd714993b3a6911664e4eb9087e2c8fe8e0ce7f8c872120d225139b1
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.13

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 Oct 14, 2026.

Transparency log

Release files / django_zeal-2.3.0-py3-none-any.whl

Download URL django_zeal-2.3.0-py3-none-any.whl
Size 16.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
fdee7ca2056d1ec58be01cee39da6e2cd56664f4e9073cc1ad8d633acd133437
BLAKE2b-256 checksum
How to use checksums
e1b8fe7ec475385586c835ede79f742562fbb95afcb0bde7b4f5a7a3b70a81a1
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.13

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 Oct 14, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

2.3.0 This release

2 release files

2.2.4

2 release files

2.2.3

2 release files

2.2.2

2 release files

2.2.1

2 release files

2.2.0

2 release files

2.1.0

2 release files

2.0.5

2 release files

2.0.4

2 release files

2.0.3

2 release files

2.0.2

2 release files

2.0.1

2 release files

2.0.0

2 release files

1.4.1

2 release files

1.4.0

2 release files

1.3.0

2 release files

1.2.0

2 release files

1.1.0

2 release files

1.0.0

1 release file

0.2.3

1 release file

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