Skip to main content

nplusone is a library for detecting the n+1 queries problem in Python ORMs, including SQLAlchemy, Peewee, and the Django ORM

Project description

nplusone

Detect the N+1 queries problem in Python ORMs — SQLAlchemy, Peewee, and Django ORM.

A modern rewrite of the original nplusone library, targeting Python 3.11+ with full type annotations, SQLAlchemy 2.0 support, and fixes for false positives found in production Django+DRF codebases.

PyPI Python Tests Coverage

Installation

pip install nplus1

With optional ORM/framework dependencies:

pip install nplus1[django]           # Django 4.2+
pip install nplus1[sqlalchemy]       # SQLAlchemy 2.0+
pip install nplus1[flask]            # Flask + Flask-SQLAlchemy
pip install nplus1[peewee]           # Peewee 3.15+

Quick Start

Django

Add the middleware to your dev settings:

# settings/dev.py
MIDDLEWARE = [
    ...
    "nplusone.ext.django.NPlusOneMiddleware",
    ...
]

NPLUSONE_ENABLED = True    # set False in prod (zero overhead)
NPLUSONE_LOG = True        # log detections (default)
NPLUSONE_RAISE = True      # raise exceptions in dev/test

Flask + SQLAlchemy

from nplusone.ext.flask_sqlalchemy import NPlusOne

app = Flask(__name__)
NPlusOne(app)

Standalone (any code)

from nplusone.core.profiler import Profiler

with Profiler():
    users = session.query(User).all()
    for user in users:
        user.addresses  # NPlusOneError raised

Celery

from nplusone.ext.celery import NPlusOneCelery

app = Celery("myapp")
NPlusOneCelery(app)

Or manually with signals:

from celery.signals import task_prerun, task_postrun
from nplusone.core.profiler import setup, teardown

@task_prerun.connect()
def on_prerun(**kwargs):
    setup()

@task_postrun.connect()
def on_postrun(**kwargs):
    teardown()

What It Detects

N+1 lazy loads

users = User.objects.all()          # 1 query
for user in users:
    print(user.addresses)           # N queries — flagged!

Fix: use select_related or prefetch_related:

users = User.objects.select_related("addresses").all()

Unnecessary eager loads

users = User.objects.select_related("occupation").all()
for user in users:
    print(user.name)                # occupation never accessed — flagged!

Configuration

All settings work across Django, Flask, and Celery:

Setting Default Description
NPLUSONE_ENABLED True Master switch. Set False in prod for zero overhead.
NPLUSONE_LOG True Log detections to the nplusone logger.
NPLUSONE_RAISE False Raise NPlusOneError on detection.
NPLUSONE_WHITELIST [] List of rule dicts to suppress specific warnings.
NPLUSONE_LOGGER logging.getLogger("nplusone") Custom logger instance.
NPLUSONE_LOG_LEVEL DEBUG Log level for detections.
NPLUSONE_DEBUG False Verbose signal logging to nplusone.debug logger.
NPLUSONE_REPORT_MODE "immediate" "immediate" or "batch". Batch collects all detections and reports at end of request.
NPLUSONE_SKIP_EAGER_ON_ERROR True Skip eager load checks on error responses (>= 400).
NPLUSONE_EAGER_LOAD_SKIP None Callable (request, response) -> bool for custom skip logic.
NPLUSONE_SKIP_EMPTY_PREFETCH False Skip flagging prefetch_related that returns zero rows.
NPLUSONE_EXCLUDE_URLS [] List of URL prefixes to skip all detection for (e.g. ["/admin/"]).

Whitelisting

Suppress specific warnings by model, field, or pattern:

NPLUSONE_WHITELIST = [
    {"model": "User", "field": "profile"},       # exact match
    {"model": "myapp.User"},                      # Django app_label.Model format
    {"model": "User*"},                           # fnmatch wildcard
    {"label": "unused_eager_load"},               # suppress all eager load warnings
]

URL Exclusion

Skip detection entirely for URL prefixes where detections are unfixable (e.g. Django admin internals):

NPLUSONE_EXCLUDE_URLS = [
    "/admin/",
    "/debug/",
]

Prod/Dev Split

Only add the middleware in dev/test settings — no need for it in production:

# settings/dev.py (or settings/test.py)
MIDDLEWARE = [
    ...
    "nplusone.ext.django.NPlusOneMiddleware",
    ...
]
NPLUSONE_RAISE = True

No INSTALLED_APPS entry is needed — the ORM patches are applied automatically when the middleware is imported.

For Celery, use NPLUSONE_ENABLED to control whether detection runs:

# settings/base.py
NPLUSONE_ENABLED = False   # Celery setup() is a no-op

# settings/dev.py
NPLUSONE_ENABLED = True    # Celery detection active

Debug Mode

Enable NPLUSONE_DEBUG = True to see every signal fire during a request:

[nplusone.debug] REQUEST START: GET /api/orders/
[nplusone.debug] EAGER_REGISTER: Order.customer (5 instances) at views.py:42 in get_queryset
[nplusone.debug] EAGER_ACCESS: Order.customer (1 instances) at serializers.py:18 in to_representation
[nplusone.debug] DETECTED: Potential unnecessary eager load on Order.shipping_address
[nplusone.debug] REQUEST END: GET /api/orders/ → 200

Detection messages include the registration site (inspired by django-zeal's ZEAL_SHOW_ALL_CALLERS):

Potential unnecessary eager load detected on `Order.shipping_address`
  Registered at: myapp/views.py:42 in get_queryset
                 qs.select_related("customer", "shipping_address")

Comparison

vs. jmcarp/nplusone (original)

This library is a ground-up rewrite of the original nplusone, which has been unmaintained since 2020. We preserve the same detection architecture (blinker signals + ORM monkey-patching) but modernize everything else:

Original nplusone This library
Python 2.7+ / 3.3+ 3.11+
Type hints None Full (mypy strict + pyright strict)
SQLAlchemy 1.x only 2.0+
Django 1.8+ (compat code) 4.2 – 5.2 (clean)
Nullable FK False positive Skipped (valid optimization)
MTI / Polymorphic False positives PK-based cross-model matching
Error responses False positive Skipped on 4xx/5xx (configurable)
Celery Not supported NPlusOneCelery(app) + setup()/teardown()
Debug/trace mode Not available NPLUSONE_DEBUG with full signal logging
Stack traces Not in messages Registration site in every detection
Batch reporting Not available NPLUSONE_REPORT_MODE = "batch"
Prod switch Not available NPLUSONE_ENABLED = False (zero overhead)
Dependencies six, blinker blinker only

vs. django-zeal

django-zeal is a Django-only N+1 detector with a different approach.

django-zeal This library
ORMs Django only Django, SQLAlchemy, Peewee
Detect N+1 lazy loads Yes Yes
Detect unused eager loads No Yes
Detect .defer()/.only() issues Yes No
Configurable threshold Yes (ZEAL_NPLUSONE_THRESHOLD) No (flags on first repeat)
Non-invasive in prod Yes (no patching when inactive) Yes (NPLUSONE_ENABLED = False skips all setup)
Stack traces Yes (ZEAL_SHOW_ALL_CALLERS) Yes (always included)
Celery Manual setup()/teardown() NPlusOneCelery(app) or manual
Batch reporting No Yes

Choose nplusone if you need multi-ORM support, unused eager load detection, or work with complex Django patterns (MTI, polymorphic models, DRF).

Choose django-zeal if you only use Django and want .defer()/.only() detection or configurable thresholds.

Development

# Setup
uv sync

# Run tests
python -m pytest tests/

# Run tests for specific ORM
tox -e py311-django52
tox -e py311-sqlalchemy
tox -e py311-peewee
tox -e py311-flask

# Lint and type check
ruff check nplusone/ tests/
python -m mypy nplusone/
npx pyright

# Coverage
python -m pytest tests/ --cov=nplusone --cov-report=term-missing

Multi-version Testing

# Full matrix
tox

# Specific Python + Django version
tox -e py312-django51
tox -e py313-django42

Docker (PostgreSQL)

docker compose up -d
cp .env.EXAMPLE .env
python -m pytest tests/testapp/

License

MIT. See LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

nplus1-1.1.0.tar.gz (22.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

nplus1-1.1.0-py3-none-any.whl (30.6 kB view details)

Uploaded Python 3

File details

Details for the file nplus1-1.1.0.tar.gz.

File metadata

  • Download URL: nplus1-1.1.0.tar.gz
  • Upload date:
  • Size: 22.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for nplus1-1.1.0.tar.gz
Algorithm Hash digest
SHA256 58c5013e74e3d1cb1656ed6fd7fc5a83c5188aca6fbf5d918b8343843d41ea9e
MD5 5a5bc1381ddf21a59448f34e7db4115a
BLAKE2b-256 76274114ce7fd262df4aa9ce3527e3292697fdc0663d7e3f4aa6c7ce7615b3f2

See more details on using hashes here.

Provenance

The following attestation bundles were made for nplus1-1.1.0.tar.gz:

Publisher: publish.yml on huynguyengl99/nplus1

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file nplus1-1.1.0-py3-none-any.whl.

File metadata

  • Download URL: nplus1-1.1.0-py3-none-any.whl
  • Upload date:
  • Size: 30.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for nplus1-1.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 af6182d62f94d3f61cd13e8b5d1ff0ad2543ac91b931a7d590040b2f3ad025f4
MD5 2eba28d412c6b4368e1eac3243fb5a5e
BLAKE2b-256 7305fb11020ef2ddf5001ab39f1d50731a8701c3a72bd945c94dce03e185466e

See more details on using hashes here.

Provenance

The following attestation bundles were made for nplus1-1.1.0-py3-none-any.whl:

Publisher: publish.yml on huynguyengl99/nplus1

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page