Django

Builds, debugs, and hardens Django apps: models, the ORM, views, templates, forms, the admin, DRF APIs, and deployment. Use when a page fires one query per row (N+1, select_related…

Iván

@ivangdavila

What This Skill Does

Builds, debugs, and hardens Django applications across models, ORM, views, templates, forms, admin, DRF APIs, and deployment. Covers common pitfalls like N+1 queries, migration conflicts, production configuration errors, and async ORM access.

Replaces hours of manual debugging and scattered Django documentation by providing targeted solutions for the most frequent framework-specific issues.

When to Use It

  • Fix N+1 queries by applying select_related or prefetch_related to reduce database hits
  • Resolve migration conflicts after a merge using makemigrations --merge
  • Debug a 403 CSRF error by checking for missing template tag or misconfigured CSRF_TRUSTED_ORIGINS
  • Harden a production Django app with check --deploy and proper ALLOWED_HOSTS settings
  • Handle SynchronousOnlyOperation when accessing the ORM from an async view or channel consumer
  • Prevent duplicate rows from chained filter calls by combining conditions in a single filter

Install

$ openclaw skills install @ivangdavila/django

User preferences and memory live in ~/Clawic/data/django/ (see setup.md on first use, memory-template.md for the file format). If you have data at an old location (~/django/ or ~/clawic/django/), move it to ~/Clawic/data/django/.

When To Use

  • Writing or reviewing Django models, migrations, views, forms, templates, admin classes, or DRF serializers
  • A page is slow or the query count grows with the number of rows on screen
  • A migration will not generate, will not apply, conflicts after a merge, or would lock a production table
  • An exception that is Django's and not Python's: SynchronousOnlyOperation, TransactionManagementError, AppRegistryNotReady, NoReverseMatch, DisallowedHost, ImproperlyConfigured
  • Hardening a project for production: settings split, check --deploy, static and media, sessions, permissions, upload limits
  • Background jobs, async views, Channels, caching, or a test suite that is slow or order-dependent
  • Not for plain Python semantics, packaging, or asyncio internals, and not for engine-level SQL tuning (see Related Skills)

Quick Reference

SituationPlay
Query count grows with rows on the pageselect_related for forward FK/O2O, prefetch_related for reverse FK/M2M (Core Rules 1-2, → orm.md)
Sum/Count inflated after annotateTwo joins multiply rows — Count("x", distinct=True) or a Subquery (→ orm.md)
Rows come back duplicated after filtering on a related modelChained .filter().filter() joins twice; one .filter(a=..., b=...) requires the same related row (→ orm.md)
makemigrations reports "No changes detected"App missing from INSTALLED_APPS, or models defined outside an imported module (→ migrations.md)
Two migration leaves after a mergemakemigrations --merge; never renumber files by hand (→ migrations.md)
The migration must run on a live tableExpand → backfill in batches → contract, each in its own migration (Core Rules 6, → migrations.md)
403 "CSRF verification failed"Missing {% csrf_token %}, or CSRF_TRUSTED_ORIGINS entries without a scheme behind a proxy (→ security.md)
400 on every request once DEBUG=FalseALLOWED_HOSTS (→ settings.md)
500 with an empty response and nothing in the logsDEBUG off with no LOGGING config — the exception exists, nothing writes it down (→ settings.md)
Redirect loop behind a load balancerSECURE_SSL_REDIRECT without SECURE_PROXY_SSL_HEADER (→ deployment.md)
SynchronousOnlyOperationORM touched from an async context — sync_to_async or the a-prefixed ORM methods (→ async.md)
Task fails with DoesNotExist, then succeeds on retryQueued inside atomic() and picked up before COMMIT — transaction.on_commit (Core Rules 5, → tasks.md)
Admin change page hangs or times outA ForeignKey rendered as a <select> of every row — autocomplete_fields, list_select_related (→ admin.md)
Static files 404, or the manifest raises after deploycollectstatic, STATIC_ROOT, and hashed-name references (→ deployment.md)
Tests pass alone and fail as a suiteMutated setUpTestData objects, or a setting read at import time (→ testing.md)
A DRF endpoint issues N+1 or leaks a fieldSerializerMethodField touching a relation; fields = "__all__" (→ drf.md)
Login, permissions, or a custom user modelauth.md — and set AUTH_USER_MODEL before the first migrate (Core Rules 8)
Starting a project, or deciding where a new app goesstartproject config ., domain-shaped apps, and a label chosen once — it is baked into every table name (→ layout.md)
Bumping the Django version, or RemovedInDjangoXXWarning in the test outputClear deprecations on the current version with python -Wa manage.py test, then move one feature release at a time (→ upgrade.md)
Text must render in the user's language, or dates in their formatgettext_lazy at import time, {% blocktranslate %} in templates, and compilemessages — Django reads .mo, never .po (→ i18n.md)
Anything elseReproduce in manage.py shell, switch the django.db.backends logger to DEBUG, and read the SQL Django actually emitted before changing any code (→ debug.md)

Depth on demand, by phase:

  • Startlayout.md project skeleton, app boundaries, labels, where non-app code goes
  • Diagnosedebug.md symptom to cause in minutes · commands.md the manage.py toolkit and what each command really does
  • Model the datamodels.md fields, relations, constraints, managers, signals · migrations.md generating, merging, squashing, online schema change · orm.md querysets, joins, aggregation, transactions, locking
  • Serve requestsviews.md view classes, URLs, middleware, requests and responses · forms.md validation, formsets, file uploads · templates.md escaping, context, custom tags · auth.md users, sessions, permissions, password flows · admin.md the admin at real data volume · drf.md serializers, viewsets, permissions, pagination · i18n.md translation, locale switching, formats, timezones
  • Make it fastperformance.md query budgets, caching layers, profiling · async.md async views, ASGI, Channels · tasks.md background jobs, on_commit, retries, email
  • Ship itsettings.md settings layout, env config, logging, timezone · deployment.md WSGI/ASGI, workers, static and media, release sequence · security.md the Django-specific attack surface · testing.md fast, isolated, honest tests · upgrade.md release cadence, deprecations, LTS windows

Core Rules

  1. Give every list view a query budget and assert it. Budget = 1 query for the page + 1 per prefetch_related + 0 for select_related (it joins into the page query) + 1 for the count if you paginate. A paginated 50-row page of orders with select_related("customer") and prefetch_related("items") is 1 + 1 + 0 + 1 = 3 queries; the unoptimized version of the same page is 1 + 50 + 50 + 1 = 102. Check it with assertNumQueries(3) in a test, not by eye — the regression arrives inside someone else's template change.
  2. select_related joins, prefetch_related runs a second query. Forward ForeignKey/OneToOneFieldselect_related (SQL JOIN, one query). Reverse FK and ManyToManyFieldprefetch_related (one extra query, joined in Python). Passing an M2M to select_related raises FieldError; passing a forward FK to prefetch_related works but buys an extra round trip for nothing.
  3. Queryset-level writes bypass the model. update(), delete(), bulk_create(), bulk_update() never call Model.save(), never fire pre_save/post_save, never touch auto_now, and never run validators. That is exactly why they are fast. When you use them, set the timestamp yourself: .update(status="done", updated_at=timezone.now()).
  4. Counters use F(), not read-modify-write. obj.n += 1; obj.save() reads a stale value and loses every concurrent increment; Model.objects.filter(pk=pk).update(n=F("n") + 1) is a single atomic UPDATE ... SET n = n + 1. After an F() write the in-memory attribute holds an expression object, not a number — refresh_from_db() before reading it.
  5. Side effects belong in transaction.on_commit. Anything outside the database — a queued task, an email, a webhook, a cache invalidation — fires only after COMMIT. Queued inside atomic(), a worker can pick the job up before the row is visible: the symptom is a task failing with DoesNotExist for an object you just created, and passing on retry.
  6. A schema change on a live table is three deploys, not one. Expand (add the nullable column or new table, ship code that tolerates both shapes) → backfill in batches with a resume key → contract (set NOT NULL, drop the old column) once nothing reads the old shape. One migration that adds a NOT NULL column to a large table rewrites it under a lock, and every request queues behind that lock.
  7. Catch database errors outside the atomic() block. After any statement raises inside a transaction, the connection is poisoned: every later query raises TransactionManagementError until rollback. To continue after an expected IntegrityError, wrap just the risky statement in its own nested with transaction.atomic(): — the nesting is a savepoint, and only the savepoint rolls back.
  8. Set AUTH_USER_MODEL before the first migrate. Start every project with class User(AbstractUser): pass even if it stays empty. Swapping the user model after tables exist means rewriting every FK to auth.User and, in practice, rebuilding migration history — Django offers no supported path for it.
  9. Reference models by string; never import them at module import time. ForeignKey("shop.Order") and settings.AUTH_USER_MODEL break import cycles. get_user_model() or a queryset at module level raises AppRegistryNotReady; put it inside the function, or in AppConfig.ready() for signal registration only.

Exception To Cause

Django raises its own exception types before Python's. The type names the subsystem.

ExceptionWhat it actually meansFirst move
SynchronousOnlyOperationAn ORM call reached an async contextWrap in sync_to_async(...), or use aget/acreate/async for (Django >=4.1) (→ async.md)
TransactionManagementErrorA query ran after an error inside atomic(), or select_for_update() ran outside a transactionRule 7; for locking, open an atomic() block first
AppRegistryNotReadyModels or get_user_model() touched during importRule 9 — move it into a function or AppConfig.ready()
ImproperlyConfiguredSettings used before django.setup(), or a required setting missing or emptyThe message tail names the setting; standalone scripts need django.setup() before importing any app code
DisallowedHostThe Host header is not in ALLOWED_HOSTSAdd the host; behind a proxy also check USE_X_FORWARDED_HOST (→ settings.md)
NoReverseMatchA {% url %}/reverse() name, namespace, or argument count is wrongCheck app_name plus the pattern's converters — a <int:pk> route rejects a string silently (→ views.md)
TemplateDoesNotExistLoader order, not a missing file, most of the timeThe debug page lists every path tried; check APP_DIRS and DIRS (→ templates.md)
FieldErrorAn invalid lookup, or only()/defer() conflicting with select_relatedThe message lists the valid choices; re-read the __ lookup chain
RelatedObjectDoesNotExistA nullable FK that is NULL, or a reverse OneToOne with no rowgetattr(obj, "profile", None); the class also catches as Model.DoesNotExist
MultipleObjectsReturnedget() matched more than one row — a uniqueness constraint is missingAdd the UniqueConstraint, then decide whether the caller wanted filter().first()
SuspiciousFileOperationA generated path escaped the storage rootNever build upload_to or a storage name from raw user input (→ security.md)
InconsistentMigrationHistoryA migration is recorded as applied before a dependency it needsUsually a late user-model swap or a re-pointed FK; repair the graph, do not --fake blindly (→ migrations.md)
OperationalError: database is lockedSQLite with concurrent writersSQLite serializes writes; raise timeout in DATABASES["default"]["OPTIONS"], or move to Postgres for anything concurrent

HTTP Symptoms

ResponseUsual cause
400 on everything after DEBUG=FalseALLOWED_HOSTS empty or missing this host
403 "CSRF verification failed"No {% csrf_token %}; a cross-origin POST needing CSRF_TRUSTED_ORIGINS entries with the scheme (https://app.example.com, required since Django >=4.0); or CSRF_COOKIE_SECURE on a plain-HTTP origin
404 on a URL that existsTrailing-slash mismatch, include() ordering, or a path converter rejecting the value
301 loopSECURE_SSL_REDIRECT behind a TLS-terminating proxy with no SECURE_PROXY_SSL_HEADER
302 to /accounts/login/ from an API clientLoginRequiredMixin on an endpoint that should answer 401/403 — use DRF permissions instead (→ drf.md)
A POST arrives as a GET with no dataAPPEND_SLASH: Django answers a slash-less POST with a 301 and the body is dropped. Post to the exact URL
500, blank body, nothing loggedDEBUG=False with default logging — Django mails ADMINS and writes nothing else (→ settings.md)
502/504 under load, fine when idleWorker saturation, or a request longer than the proxy timeout (→ deployment.md)
Users randomly logged outSECRET_KEY differs between instances, or was rotated without SECRET_KEY_FALLBACKS (Django >=4.1)

Settings Defaults That Bite

Exact Django defaults that produce confusing failures. All are overridable in settings.

SettingDefaultWhat the default costs you
DATA_UPLOAD_MAX_MEMORY_SIZE2621440 bytes (2.5 MB)A non-file POST body above it raises RequestDataTooBig — hits large JSON payloads and long text fields
DATA_UPLOAD_MAX_NUMBER_FIELDS1000TooManyFieldsSent on large formsets. A formset posts forms × fields_per_form + 4 management inputs, so 1000 caps you near 200 forms of 5 fields
FILE_UPLOAD_MAX_MEMORY_SIZE2621440 bytes (2.5 MB)Below it an upload is an in-memory object with no temporary_file_path(); above it, a temp file on disk. Code that assumes one shape breaks on the other
CONN_MAX_AGE0A fresh TCP connect plus auth handshake on every single request
CACHES["default"]["TIMEOUT"]300 secondsAnything cached without an explicit timeout expires in five minutes
LocMemCache MAX_ENTRIES300, with CULL_FREQUENCY 3At 300 keys it evicts one third at random — and each worker process holds its own copy, which is why hit rates look impossible (→ performance.md)
SESSION_COOKIE_AGE1209600 seconds (14 days)Sessions live two weeks and the django_session table grows forever unless clearsessions runs on a schedule
PASSWORD_RESET_TIMEOUT259200 seconds (3 days)Reset links stay valid for three days
Formset max_num1000, with absolute_max = max_num + 1000A crafted POST can force Django to build up to absolute_max forms before validation runs
DEFAULT_AUTO_FIELDunset → models.W042Every app gets a 32-bit AutoField and the system check nags; set BigAutoField project-wide
DEBUGFalseRight for production, and the one default people expect backwards: with DEBUG=True Django appends every query to connection.queries forever, so a long-running dev process grows without bound

Configuration

User-dependent variables. Defaults apply until the user states a preference; store them in ~/Clawic/data/django/config.yaml.

VariableTypeDefaultEffect
django_versionnumber (4.2-6.x)5.2Which Django >=X.Y gated advice applies when the project's version is unknown, and which deprecations to flag
databasepostgres | mysql | sqlite | oraclepostgresSwitches ORM and migration advice: select_for_update options, server-side cursors, JSON lookups, whether __date needs loaded timezone tables
api_layernone | drf | ninja | plain-jsondrfWhich request/response idiom generated endpoints use, and whether drf.md guidance applies at all
settings_layoutsingle | split-by-env | env-varssplit-by-envWhere a new setting is written and how secrets are read (→ settings.md)
project_layoutflat | apps-packageflatWhere a new app is created and which dotted names appear in INSTALLED_APPS and AppConfig.name (→ layout.md)
task_queuenone | celery | rq | django-tasksceleryShape of background-job examples; with none, work is inlined behind transaction.on_commit instead (→ tasks.md)
test_runnerdjango | pytest-djangodjangoWhether tests are emitted as TestCase classes or pytest functions with fixtures (→ testing.md)
deploy_targetgunicorn-wsgi | uvicorn-asgi | paas | serverlessgunicorn-wsgiWorker-count formula, static-file strategy, and whether long-lived database connections are safe (→ deployment.md)
destructive_confirmbooltruemigrate --fake, flush, sqlflush, reverse migrations and drop-column operations are emitted for review instead of run

Preference areas — customizable dimensions; a stated preference is recorded in config.yaml and applied from then on:

  • Tooling — dependency manager and venv layout, debug toolbar vs profiler, django-filter/factory_boy/allauth and friends, migration linting in CI
  • Thresholds — query budget per view, default page size, cache TTLs, backfill batch size, the slow-request threshold worth reporting
  • Conventions — fat models vs a service layer, URL and view naming, related_name style, serializer naming, app naming style
  • Platform — database engine and version, cache and broker backends, media storage backend, hosting target, Python version floor
  • Risk posture — whether migrations may be applied directly, whether raw SQL is allowed, how hard to push back on fields = "__all__" and @csrf_exempt
  • Output format — whole files vs diffs, how much explanation ships with generated code, type hints and docstrings
  • Work order — test-first vs code-first, whether a migration review gate precedes merge, when check --deploy runs
  • Integrations — auth provider and SSO, email and payment providers, error tracking, broker choice, object storage
  • Restrictions — banned packages, LTS-only policy, PII fields that must never be logged, compliance regimes requiring audit trails
  • Cadence — dependency and security upgrade rhythm, LTS upgrade window, session and log cleanup schedules

Output Gates

Before emitting models, a migration, a view, or a serializer:

  • Does every view that lists related data declare its query budget, with select_related/prefetch_related to match (Rule 1)?
  • Does the migration touch a live table, and if so, is it split expand → backfill → contract (Rule 6)?
  • Is every external side effect wrapped in transaction.on_commit (Rule 5)?
  • Do new foreign keys and frequently filtered columns get an index in the same migration?
  • Are ModelForm and ModelSerializer field lists explicit, never "__all__"?
  • Does every object fetched by an ID from the request also filter on ownership or permission (→ security.md)?
  • Are user-supplied strings rendered without |safe/mark_safe, and JSON handed to scripts through {{ data|json_script:"id" }}?
  • Timestamps via timezone.now() / timezone.localdate(), never datetime.now() / date.today()?

Traps

TrapWhy it failsDo instead
Assuming Model.save() validatessave() never calls full_clean(): choices, validators and most max_length checks are form-layer onlyEnforce in the database with Meta.constraints, or call full_clean() explicitly
null=True on a text fieldTwo empty states ("" and NULL) that every query then has to handleblank=True alone; keep null=True for non-text columns
Meta.ordering on a busy modelEvery query inherits the sort — and in values().annotate() the ordering column silently joins the GROUP BY, changing your aggregateOrder at the queryset; .order_by() with no arguments clears an inherited sort
exclude(field=None) to find NULLsCompiles to NOT (field = NULL), which drops NULL rows instead of selecting themfilter(field__isnull=True)
queryset.delete() over millions of rowsDjango loads the objects to cascade and fire signals in PythonDelete in primary-key batches, or move the cascade into the database and own it there
get_object_or_404(Order, pk=pk) in a user-facing viewAny authenticated user can read any IDScope the lookup: get_object_or_404(Order, pk=pk, user=request.user)
fields = "__all__" on a ModelForm or ModelSerializerEvery future field becomes exposed and writable the day it is addedList fields explicitly and let that list be the review surface
@login_required on a class-based viewThe decorator wraps the class object, not the request handlerLoginRequiredMixin first in the bases, or method_decorator on dispatch
Signals carrying business logicThey fire from anywhere, are invisible at the call site, and never run for update()/bulk_create()An explicit service function; keep signals for cross-app decoupling you actually need
datetime.now() in models or viewsNaive local time; with USE_TZ=True (the default in Django >=5.0) you get a RuntimeWarning and drifted comparisonstimezone.now(), and timezone.localdate() for "today"
.raw() or .extra() built with f-stringsString interpolation is SQL injection regardless of the ORM around itBind parameters: .raw("... WHERE id = %s", [pk])
Reading request.body twiceThe stream is consumed; the second read returns b""Read once into a local, or use request.POST for form encodings
Leaving sessions to growThe django_session table has no automatic cleanupmanage.py clearsessions on a schedule, or a cache-backed session engine

Where Experts Disagree

  • Fat models vs a service layer. Model methods keep behavior next to the data and make the shell powerful; a service layer keeps transactions, side effects and orchestration in one readable place. The testable boundary: anything that spans two aggregates or touches the outside world (payments, email, tasks) belongs in a service, because that is precisely what has to be wrapped in atomic() and on_commit.
  • Signals. One camp bans them as action at a distance; the other keeps them for genuine cross-app decoupling. Both agree they are the wrong tool inside a single app, and both concede they never fire for queryset-level writes — so a signal can never be the only enforcement of an invariant.
  • DRF vs plain views for JSON. DRF earns its weight when you need content negotiation, browsable docs, permissions and pagination as policy; for a handful of endpoints it is a large surface to reason about. Boundary: a public API or more than a few endpoints → DRF or Ninja; three internal endpoints → JsonResponse with explicit validation.
  • Async Django. Async views pay off for I/O fan-out (several outbound HTTP calls per request); they buy little where the request time is ORM queries, since that path still crosses a thread. Adopt per view, not per project (→ async.md).
  • UUID vs bigint primary keys. UUIDs stop enumeration and let clients mint IDs offline; random v4 fragments the index and widens every foreign key. Common ground: exposing a sequential ID is only a problem when authorization is missing — the check protects the row, not the shape of the key.

Related Skills

More Clawic skills, get them at https://clawic.com/skills/django (install if the user confirms):

  • py — Python itself: imports, packaging, typing, asyncio internals, pytest mechanics
  • pg — PostgreSQL underneath the ORM: EXPLAIN plans, index design, vacuum, locks, connection pooling
  • rest-api — API design decisions above the framework: versioning, contracts, error shapes
  • fastapi — when the service is async-first and needs no ORM, admin, or templates
  • auth — protocol-level identity: OAuth flows, SSO, MFA, passwordless

Feedback

Part of Clawic, the verified skill library. Get this skill: https://clawic.com/skills/django.

Top skills in this category