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.1.tar.gz (23.1 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.1-py3-none-any.whl (30.9 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: nplus1-1.1.1.tar.gz
  • Upload date:
  • Size: 23.1 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.1.tar.gz
Algorithm Hash digest
SHA256 8e0929b1a5b564ff025c16a79da38e323c18dd1c4cb3180edcdb1a5cf993c9e2
MD5 db31c6f76cfb26f90032a75034a48315
BLAKE2b-256 b4be0038a1b90fc7c2a284502dfcb60dad3de9a321a4d5efbf87fe80b5c8b69c

See more details on using hashes here.

Provenance

The following attestation bundles were made for nplus1-1.1.1.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.1-py3-none-any.whl.

File metadata

  • Download URL: nplus1-1.1.1-py3-none-any.whl
  • Upload date:
  • Size: 30.9 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.1-py3-none-any.whl
Algorithm Hash digest
SHA256 403f9438944671dd0272270ca508a37e50d8486b5c4047a92a08c9c0162b2155
MD5 b729a7a4283085f113d27419d2bcb8e6
BLAKE2b-256 d4d9586354155a1f7bf7ed00bc3e1300691e5fe0d932afc05a86d8992fb406e0

See more details on using hashes here.

Provenance

The following attestation bundles were made for nplus1-1.1.1-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