原始内容
name: Django slug: django version: 1.0.3 description: >- 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, prefetch_related, annotate double-counting); when makemigrations conflicts, a migration locks a live table, or InconsistentMigrationHistory blocks a deploy; on 403 CSRF verification failed, DEBUG=False turning every request into 400 DisallowedHost or a blank 500, or SECURE_SSL_REDIRECT looping behind a proxy; on SynchronousOnlyOperation, AppRegistryNotReady, or NoReverseMatch; when a background task runs before its transaction commits, signals fire on rows that roll back, or update() skips auto_now; when the admin times out on a big table, collectstatic breaks static files, or workers exhaust database connections; when writing serializers, formsets, a custom user model, permissions, or assertNumQueries tests; or upgrading Django across a deprecation. Not for plain Python, FastAPI or Flask services, or engine-level SQL tuning. homepage: https://clawic.com/skills/django changelog: "Display name shown correctly" metadata: clawdbot: emoji: 🌿 requires: bins: - python3 os: - linux - darwin - win32 displayName: Django configPaths: - ~/Clawic/data/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
| Situation | Play |
|---|---|
| Query count grows with rows on the page | select_related for forward FK/O2O, prefetch_related for reverse FK/M2M (Core Rules 1-2, → orm.md) |
Sum/Count inflated after annotate |
Two joins multiply rows — Count("x", distinct=True) or a Subquery (→ orm.md) |
| Rows come back duplicated after filtering on a related model | Chained .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 merge | makemigrations --merge; never renumber files by hand (→ migrations.md) |
| The migration must run on a live table | Expand → 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=False |
ALLOWED_HOSTS (→ settings.md) |
| 500 with an empty response and nothing in the logs | DEBUG off with no LOGGING config — the exception exists, nothing writes it down (→ settings.md) |
| Redirect loop behind a load balancer | SECURE_SSL_REDIRECT without SECURE_PROXY_SSL_HEADER (→ deployment.md) |
SynchronousOnlyOperation |
ORM touched from an async context — sync_to_async or the a-prefixed ORM methods (→ async.md) |
Task fails with DoesNotExist, then succeeds on retry |
Queued inside atomic() and picked up before COMMIT — transaction.on_commit (Core Rules 5, → tasks.md) |
| Admin change page hangs or times out | A ForeignKey rendered as a <select> of every row — autocomplete_fields, list_select_related (→ admin.md) |
| Static files 404, or the manifest raises after deploy | collectstatic, STATIC_ROOT, and hashed-name references (→ deployment.md) |
| Tests pass alone and fail as a suite | Mutated setUpTestData objects, or a setting read at import time (→ testing.md) |
| A DRF endpoint issues N+1 or leaks a field | SerializerMethodField touching a relation; fields = "__all__" (→ drf.md) |
| Login, permissions, or a custom user model | auth.md — and set AUTH_USER_MODEL before the first migrate (Core Rules 8) |
| Starting a project, or deciding where a new app goes | startproject 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 output |
Clear 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 format | gettext_lazy at import time, {% blocktranslate %} in templates, and compilemessages — Django reads .mo, never .po (→ i18n.md) |
| Anything else | Reproduce 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:
- Start —
layout.mdproject skeleton, app boundaries, labels, where non-app code goes - Diagnose —
debug.mdsymptom to cause in minutes ·commands.mdthemanage.pytoolkit and what each command really does - Model the data —
models.mdfields, relations, constraints, managers, signals ·migrations.mdgenerating, merging, squashing, online schema change ·orm.mdquerysets, joins, aggregation, transactions, locking - Serve requests —
views.mdview classes, URLs, middleware, requests and responses ·forms.mdvalidation, formsets, file uploads ·templates.mdescaping, context, custom tags ·auth.mdusers, sessions, permissions, password flows ·admin.mdthe admin at real data volume ·drf.mdserializers, viewsets, permissions, pagination ·i18n.mdtranslation, locale switching, formats, timezones - Make it fast —
performance.mdquery budgets, caching layers, profiling ·async.mdasync views, ASGI, Channels ·tasks.mdbackground jobs, on_commit, retries, email - Ship it —
settings.mdsettings layout, env config, logging, timezone ·deployment.mdWSGI/ASGI, workers, static and media, release sequence ·security.mdthe Django-specific attack surface ·testing.mdfast, isolated, honest tests ·upgrade.mdrelease cadence, deprecations, LTS windows
Core Rules
- Give every list view a query budget and assert it. Budget = 1 query for the page + 1 per
prefetch_related+ 0 forselect_related(it joins into the page query) + 1 for the count if you paginate. A paginated 50-row page of orders withselect_related("customer")andprefetch_related("items")is 1 + 1 + 0 + 1 = 3 queries; the unoptimized version of the same page is 1 + 50 + 50 + 1 = 102. Check it withassertNumQueries(3)in a test, not by eye — the regression arrives inside someone else's template change. select_relatedjoins,prefetch_relatedruns a second query. ForwardForeignKey/OneToOneField→select_related(SQL JOIN, one query). Reverse FK andManyToManyField→prefetch_related(one extra query, joined in Python). Passing an M2M toselect_relatedraisesFieldError; passing a forward FK toprefetch_relatedworks but buys an extra round trip for nothing.- Queryset-level writes bypass the model.
update(),delete(),bulk_create(),bulk_update()never callModel.save(), never firepre_save/post_save, never touchauto_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()). - 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 atomicUPDATE ... SET n = n + 1. After anF()write the in-memory attribute holds an expression object, not a number —refresh_from_db()before reading it. - 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 insideatomic(), a worker can pick the job up before the row is visible: the symptom is a task failing withDoesNotExistfor an object you just created, and passing on retry. - 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.
- Catch database errors outside the
atomic()block. After any statement raises inside a transaction, the connection is poisoned: every later query raisesTransactionManagementErroruntil rollback. To continue after an expectedIntegrityError, wrap just the risky statement in its own nestedwith transaction.atomic():— the nesting is a savepoint, and only the savepoint rolls back. - Set
AUTH_USER_MODELbefore the firstmigrate. Start every project withclass User(AbstractUser): passeven if it stays empty. Swapping the user model after tables exist means rewriting every FK toauth.Userand, in practice, rebuilding migration history — Django offers no supported path for it. - Reference models by string; never import them at module import time.
ForeignKey("shop.Order")andsettings.AUTH_USER_MODELbreak import cycles.get_user_model()or a queryset at module level raisesAppRegistryNotReady; put it inside the function, or inAppConfig.ready()for signal registration only.
Exception To Cause
Django raises its own exception types before Python's. The type names the subsystem.
| Exception | What it actually means | First move |
|---|---|---|
SynchronousOnlyOperation |
An ORM call reached an async context | Wrap in sync_to_async(...), or use aget/acreate/async for (Django >=4.1) (→ async.md) |
TransactionManagementError |
A query ran after an error inside atomic(), or select_for_update() ran outside a transaction |
Rule 7; for locking, open an atomic() block first |
AppRegistryNotReady |
Models or get_user_model() touched during import |
Rule 9 — move it into a function or AppConfig.ready() |
ImproperlyConfigured |
Settings used before django.setup(), or a required setting missing or empty |
The message tail names the setting; standalone scripts need django.setup() before importing any app code |
DisallowedHost |
The Host header is not in ALLOWED_HOSTS |
Add the host; behind a proxy also check USE_X_FORWARDED_HOST (→ settings.md) |
NoReverseMatch |
A {% url %}/reverse() name, namespace, or argument count is wrong |
Check app_name plus the pattern's converters — a <int:pk> route rejects a string silently (→ views.md) |
TemplateDoesNotExist |
Loader order, not a missing file, most of the time | The debug page lists every path tried; check APP_DIRS and DIRS (→ templates.md) |
FieldError |
An invalid lookup, or only()/defer() conflicting with select_related |
The message lists the valid choices; re-read the __ lookup chain |
RelatedObjectDoesNotExist |
A nullable FK that is NULL, or a reverse OneToOne with no row | getattr(obj, "profile", None); the class also catches as Model.DoesNotExist |
MultipleObjectsReturned |
get() matched more than one row — a uniqueness constraint is missing |
Add the UniqueConstraint, then decide whether the caller wanted filter().first() |
SuspiciousFileOperation |
A generated path escaped the storage root | Never build upload_to or a storage name from raw user input (→ security.md) |
InconsistentMigrationHistory |
A migration is recorded as applied before a dependency it needs | Usually a late user-model swap or a re-pointed FK; repair the graph, do not --fake blindly (→ migrations.md) |
OperationalError: database is locked |
SQLite with concurrent writers | SQLite serializes writes; raise timeout in DATABASES["default"]["OPTIONS"], or move to Postgres for anything concurrent |
HTTP Symptoms
| Response | Usual cause |
|---|---|
400 on everything after DEBUG=False |
ALLOWED_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 exists | Trailing-slash mismatch, include() ordering, or a path converter rejecting the value |
| 301 loop | SECURE_SSL_REDIRECT behind a TLS-terminating proxy with no SECURE_PROXY_SSL_HEADER |
302 to /accounts/login/ from an API client |
LoginRequiredMixin on an endpoint that should answer 401/403 — use DRF permissions instead (→ drf.md) |
| A POST arrives as a GET with no data | APPEND_SLASH: Django answers a slash-less POST with a 301 and the body is dropped. Post to the exact URL |
| 500, blank body, nothing logged | DEBUG=False with default logging — Django mails ADMINS and writes nothing else (→ settings.md) |
| 502/504 under load, fine when idle | Worker saturation, or a request longer than the proxy timeout (→ deployment.md) |
| Users randomly logged out | SECRET_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.
| Setting | Default | What the default costs you |
|---|---|---|
DATA_UPLOAD_MAX_MEMORY_SIZE |
2621440 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_FIELDS |
1000 | TooManyFieldsSent 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_SIZE |
2621440 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_AGE |
0 | A fresh TCP connect plus auth handshake on every single request |
CACHES["default"]["TIMEOUT"] |
300 seconds | Anything cached without an explicit timeout expires in five minutes |
LocMemCache MAX_ENTRIES |
300, with CULL_FREQUENCY 3 |
At 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_AGE |
1209600 seconds (14 days) | Sessions live two weeks and the django_session table grows forever unless clearsessions runs on a schedule |
PASSWORD_RESET_TIMEOUT |
259200 seconds (3 days) | Reset links stay valid for three days |
Formset max_num |
1000, with absolute_max = max_num + 1000 |
A crafted POST can force Django to build up to absolute_max forms before validation runs |
DEFAULT_AUTO_FIELD |
unset → models.W042 |
Every app gets a 32-bit AutoField and the system check nags; set BigAutoField project-wide |
DEBUG |
False |
Right 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.
| Variable | Type | Default | Effect |
|---|---|---|---|
| django_version | number (4.2-6.x) | 5.2 | Which Django >=X.Y gated advice applies when the project's version is unknown, and which deprecations to flag |
| database | postgres | mysql | sqlite | oracle | postgres | Switches ORM and migration advice: select_for_update options, server-side cursors, JSON lookups, whether __date needs loaded timezone tables |
| api_layer | none | drf | ninja | plain-json | drf | Which request/response idiom generated endpoints use, and whether drf.md guidance applies at all |
| settings_layout | single | split-by-env | env-vars | split-by-env | Where a new setting is written and how secrets are read (→ settings.md) |
| project_layout | flat | apps-package | flat | Where a new app is created and which dotted names appear in INSTALLED_APPS and AppConfig.name (→ layout.md) |
| task_queue | none | celery | rq | django-tasks | celery | Shape of background-job examples; with none, work is inlined behind transaction.on_commit instead (→ tasks.md) |
| test_runner | django | pytest-django | django | Whether tests are emitted as TestCase classes or pytest functions with fixtures (→ testing.md) |
| deploy_target | gunicorn-wsgi | uvicorn-asgi | paas | serverless | gunicorn-wsgi | Worker-count formula, static-file strategy, and whether long-lived database connections are safe (→ deployment.md) |
| destructive_confirm | bool | true | migrate --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/allauthand 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_namestyle, 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 --deployruns - 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_relatedto 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
ModelFormandModelSerializerfield 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(), neverdatetime.now()/date.today()?
Traps
| Trap | Why it fails | Do instead |
|---|---|---|
Assuming Model.save() validates |
save() never calls full_clean(): choices, validators and most max_length checks are form-layer only |
Enforce in the database with Meta.constraints, or call full_clean() explicitly |
null=True on a text field |
Two empty states ("" and NULL) that every query then has to handle |
blank=True alone; keep null=True for non-text columns |
Meta.ordering on a busy model |
Every query inherits the sort — and in values().annotate() the ordering column silently joins the GROUP BY, changing your aggregate |
Order at the queryset; .order_by() with no arguments clears an inherited sort |
exclude(field=None) to find NULLs |
Compiles to NOT (field = NULL), which drops NULL rows instead of selecting them |
filter(field__isnull=True) |
queryset.delete() over millions of rows |
Django loads the objects to cascade and fire signals in Python | Delete 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 view |
Any authenticated user can read any ID | Scope the lookup: get_object_or_404(Order, pk=pk, user=request.user) |
fields = "__all__" on a ModelForm or ModelSerializer |
Every future field becomes exposed and writable the day it is added | List fields explicitly and let that list be the review surface |
@login_required on a class-based view |
The decorator wraps the class object, not the request handler | LoginRequiredMixin first in the bases, or method_decorator on dispatch |
| Signals carrying business logic | They 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 views |
Naive local time; with USE_TZ=True (the default in Django >=5.0) you get a RuntimeWarning and drifted comparisons |
timezone.now(), and timezone.localdate() for "today" |
.raw() or .extra() built with f-strings |
String interpolation is SQL injection regardless of the ORM around it | Bind parameters: .raw("... WHERE id = %s", [pk]) |
Reading request.body twice |
The stream is consumed; the second read returns b"" |
Read once into a local, or use request.POST for form encodings |
| Leaving sessions to grow | The django_session table has no automatic cleanup |
manage.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()andon_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 →
JsonResponsewith 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 mechanicspg— PostgreSQL underneath the ORM: EXPLAIN plans, index design, vacuum, locks, connection poolingrest-api— API design decisions above the framework: versioning, contracts, error shapesfastapi— when the service is async-first and needs no ORM, admin, or templatesauth— protocol-level identity: OAuth flows, SSO, MFA, passwordless
Feedback
- If useful, star it: https://clawic.com/skills/django
- Latest version: https://clawic.com/skills/django
Part of Clawic, the verified skill library. Get this skill: https://clawic.com/skills/django.