Django
Pathrule3 Rules • 3 Memories • 1 Skill
An opinionated bundle for Django on the 6.x line, where 6.1 is current and 5.2 is the LTS most teams run in production. It targets the three places Django projects actually break: N+1 queries that pass every test and fall over under load, settings and secrets that leak because DEBUG and ALLOWED_HOSTS were never split per environment, and async views that call the sync ORM and raise SynchronousOnlyOperation. Where the fastapi pattern covers pure Python APIs, this one covers the full framework: models, migrations, templates, and the settings module.
Suggested path map
Pathrule places each piece on the matching path, so your assistant only sees it where it belongs. This is the scoping you get on import; you can adjust it in your workspace.
Rules
3Split settings per environment and read every secret from the environment/confighighstrictDEBUG, SECRET_KEY, ALLOWED_HOSTS, and database credentials come from the environment, and production sets the security headers explicitly.
| 1 | Django's default settings module is built for a first run on localhost, not for production. Shipping it unchanged is how a project ends up with `DEBUG = True` in front of real users, which prints stack traces and settings to anyone who triggers an error. |
| 2 | |
| 3 | - Keep a base settings module plus per-environment overrides (or one module driven entirely by environment variables). Nothing environment-specific is committed. |
| 4 | - Read `SECRET_KEY`, database URL, and every third-party credential from the environment and fail loudly at startup when one is missing. A default value for a secret is a production incident waiting for a deploy. |
| 5 | - Set `DEBUG = False` and a real `ALLOWED_HOSTS` list in production, and turn on `SECURE_SSL_REDIRECT`, `SECURE_HSTS_SECONDS`, `SESSION_COOKIE_SECURE`, and `CSRF_COOKIE_SECURE`. |
| 6 | - Run `python manage.py check --deploy` in CI and treat its warnings as failures. |
| 7 | |
| 8 | See the secrets-env-management pattern for how the values are stored and rotated. |
Keep migrations reversible and non-blocking/appshighadvisorySchema and data changes ship as separate migrations, columns arrive nullable before they are populated, and every RunPython has a reverse.
| 1 | A migration that works on an empty dev database can hold an exclusive lock on a large production table for minutes. Treat migrations as deploys of their own. |
| 2 | |
| 3 | - Split schema changes from data backfills. A schema migration should be fast and lock-light; a backfill runs in batches and can be re-run. |
| 4 | - Add columns as nullable (or with no default that requires a table rewrite), deploy the code that writes them, backfill, then add the constraint. Never do all four in one migration. |
| 5 | - Give every `RunPython` a `reverse_code` (or `migrations.RunPython.noop`) so the migration is reversible, and never import models directly; use `apps.get_model` inside the function. |
| 6 | - Do not edit a migration that has already run in any shared environment. Add a new one. |
| 7 | - Review generated migrations before committing them; `makemigrations` will happily drop and recreate a column when a field is renamed. |
Memories
3Async views and the ORM: what is safe to await/appsAsync views run under ASGI and must use the ORM's async API; a sync query inside an async view raises SynchronousOnlyOperation.
| 1 | Django's async story is real but partial, and the boundary is where projects get hurt. The rule of thumb: an `async def` view may only touch the ORM through its async API. |
| 2 | |
| 3 | - The ORM exposes async variants (`aget`, `acreate`, `asave`, `adelete`, `afirst`, `acount`, `aupdate_or_create`) and supports `async for` over a queryset. A plain sync call inside an async context raises `SynchronousOnlyOperation`. |
| 4 | - Evaluation is still where the query happens. `qs.filter(...)` builds; `async for` or an `a`-prefixed method executes. |
| 5 | - Wrap unavoidable sync work (a third-party library, a sync-only ORM path) in `sync_to_async(..., thread_sensitive=True)` rather than calling it directly. |
| 6 | - Async only pays off when the view actually waits on I/O it can overlap: several outbound HTTP calls, a slow external API, streaming. A single query per request is faster in a plain sync view. |
| 7 | - Async views need an ASGI server (uvicorn, daphne, hypercorn) in front. Under WSGI they are run in a thread and you get the complexity without the benefit. |
| 8 | |
| 9 | See /apps for the query rule and /config for the settings rule. |
Where logic lives: fat models, thin views, explicit services/appsQuery logic belongs in managers and querysets, multi-model workflows in a service function, and views stay request-and-response only.
| 1 | Django gives you many places to put behaviour, and the difference between a maintainable project and a pile of views is choosing deliberately. |
| 2 | |
| 3 | - Row-level behaviour goes on the model. Reusable query logic goes on a custom `Manager` or `QuerySet` so it composes (`Order.objects.paid().recent()`) instead of being copy-pasted into views. |
| 4 | - A workflow that spans several models, sends mail, or calls a third party goes in a plain service function under the app, called by the view. It is testable without a request and reusable from a management command or a task. |
| 5 | - Views translate a request into a call and a response. If a view is longer than a screen, the logic belongs elsewhere. |
| 6 | - Use signals sparingly and never for business rules. `post_save` side effects are invisible at the call site and fire in tests, fixtures, and bulk operations you did not think about. Call the service explicitly. |
| 7 | - Bulk paths (`bulk_create`, `bulk_update`, `queryset.update()`) skip `save()` and signals by design. If a rule must always hold, enforce it in the database or in the service, not in `save()`. |
| 8 | |
| 9 | See /apps for the migration rule; the rest-api-design pattern covers the HTTP surface when you add DRF. |
Test setup that stays fast as the suite grows/testsBuild objects with factories instead of fixtures, reuse the test database, and assert query counts on the endpoints that matter.
| 1 | Django's test tooling is fast if you let it be, and a slow suite is the reason teams stop running it. |
| 2 | |
| 3 | - Prefer factories (`factory_boy`) over JSON fixtures. Fixtures rot silently as the schema changes and force every test to carry rows it does not use. |
| 4 | - Reuse the test database between runs (`--reuse-db` with pytest-django, `--keepdb` with the Django runner) and only rebuild when migrations change. |
| 5 | - Use `pytest.mark.django_db` (or `TestCase`) so each test runs in a transaction that rolls back. Reach for `TransactionTestCase` only when you genuinely test commit behaviour, because it truncates tables and is much slower. |
| 6 | - Guard the N+1 rule with `assertNumQueries` (or `django_assert_num_queries`) on your highest-traffic endpoints. It is the only test that fails when someone removes a `select_related`. |
| 7 | - Test at the boundary you care about: services with plain function calls, views through the test client, and templates only where rendering logic is real. |
| 8 | |
| 9 | See /apps for the query rule this suite protects. |
Skills
1django-query-review/rootPre-merge checklist for any Django change that adds a view, serializer, template, or migration.
| 1 | --- |
| 2 | name: django-query-review |
| 3 | description: Django review checklist for query efficiency, settings safety, async correctness, and migration risk. Run before merging any change that adds a view, serializer, template, or migration. |
| 4 | --- |
| 5 | |
| 6 | # Django review |
| 7 | |
| 8 | ## Queries |
| 9 | - [ ] Every relation a view, serializer, or template touches is in `select_related` or `prefetch_related`. |
| 10 | - [ ] No query inside a loop, template tag, or property called per row. |
| 11 | - [ ] Counts and aggregates run in the database (`count()`, `annotate`, `Sum`), not in Python. |
| 12 | - [ ] Large reads use `only()`/`values()` and `.iterator()` where the result set is unbounded. |
| 13 | - [ ] Any raw SQL is parameterized; no f-strings, no `.extra()`. |
| 14 | - [ ] A high-traffic endpoint has an `assertNumQueries` guard. |
| 15 | |
| 16 | ## Settings and security |
| 17 | - [ ] No new setting hardcodes an environment value or a secret. |
| 18 | - [ ] `manage.py check --deploy` still passes. |
| 19 | - [ ] User input reaches the database through a form or serializer, not straight from `request`. |
| 20 | |
| 21 | ## Async |
| 22 | - [ ] `async def` views use only the ORM's async API (`aget`, `async for`, ...). |
| 23 | - [ ] Sync-only calls are wrapped in `sync_to_async(thread_sensitive=True)`. |
| 24 | - [ ] The view actually overlaps I/O; otherwise it is a sync view. |
| 25 | |
| 26 | ## Migrations |
| 27 | - [ ] Schema change and data backfill are separate migrations. |
| 28 | - [ ] New columns land nullable, are backfilled, then constrained. |
| 29 | - [ ] Every `RunPython` has a reverse and uses `apps.get_model`. |
| 30 | - [ ] No already-applied migration was edited. |
Why this pattern
AI agents write Django views that loop over related objects and fire hundreds of queries, hardcode settings that belong in the environment, and put sync ORM calls inside async views.
Built for Python teams running Django 5.2 LTS or 6.x in production.
Keeps your assistant from:
- Iterating a queryset and touching related objects, firing one query per row
- Leaving DEBUG True or a literal SECRET_KEY in a settings file that ships
- Calling the sync ORM inside an async view and raising SynchronousOnlyOperation
- Writing a migration that locks a large table or cannot be rolled back
- License
- Apache-2.0
- Version
- 1.0.0
- Updated
- 2026-08-24