Django

djangoasyncormpythonrelease

Django is the batteries-included Python web framework: ORM, migrations, admin, auth and templates in one release, on a published schedule.

Whether to pick it - governance, release cadence, contributor base - is on Python Web Frameworks. This page is what bites once you have.

Checked against Django 6.1.2 on Python 3.15, every snippet below run.

Cookbook

See the SQL before it runs

qs = Knight.objects.filter(coconuts__gt=0)
print(qs.query)      # the SELECT Django will send
print(qs.explain())  # the database's own plan

qs.query is for reading, not for pasting into a shell: its parameters are interpolated without quoting. In a test, count queries instead of reading them:

class QueryCountTests(TestCase):
    def test_listing_is_one_query(self):
        with self.assertNumQueries(1):
            list(Knight.objects.all())

When that assertion fails it prints every query it captured, which is usually the whole answer.

Kill an N+1

Measured on three knights pointing at two quests:

for k in Knight.objects.all():                    # 4 queries: 1 + one per knight
    k.quest.name
for k in Knight.objects.select_related("quest"):  # 1 query, joined
    k.quest.name

for q in Quest.objects.all():                       # 3 queries
    list(q.knights.all())
for q in Quest.objects.prefetch_related("knights"): # 2 queries
    list(q.knights.all())

select_related follows forward foreign keys with a JOIN. prefetch_related runs a second query and joins in Python, which is what reverse relations and many-to-many need.

Deferred fields produce the same N+1, and select_related cannot help there. That is what fetch modes are for, below.

Migrations you can gate in CI

python manage.py makemigrations --check    # exit 1 if a migration is missing
python manage.py sqlmigrate quests 0001    # the SQL that migration will run
python manage.py showmigrations quests     # what is applied
python manage.py migrate quests 0001       # roll back to a given migration
python manage.py migrate quests zero       # roll back the whole app
python manage.py makemigrations quests --empty --name seed   # data migration skeleton

--check already implies a dry run: it prints the plan, exits 1 and writes nothing. Adding --dry-run alongside it is redundant (checked on 6.1.2: no file appears either way).

Write without loading rows

Knight.objects.filter(quest=grail).update(coconuts=F("coconuts") + 1)
Knight.objects.bulk_create([Knight(name="bedevere", quest=grail)])
Knight.objects.bulk_update(knights, ["coconuts"])

One query each, and none of them goes through save(). The gotchas below list what that skips.

Run code only if the transaction commits

with transaction.atomic():
    knight.save()
    transaction.on_commit(lambda: enqueue_notification(knight.pk))

Checked: the callback does not run inside the block, runs once the outermost atomic() commits, and never runs if the block raises. That is where queueing a background job belongs. A worker that picks the task up before the commit reads a row that is not there yet.

Gotchas

update(), bulk_update() and save(update_fields=...) skip auto_now. A queryset update() never goes through the field’s pre_save, and update_fields restricts the UPDATE to the columns you listed. All three checked on a model with updated_at = models.DateTimeField(auto_now=True): the timestamp does not move. Only a plain save() refreshes it.

A plain save() writes every column.

UPDATE "app_knight" SET "name" = 'Arthur', "quest_id" = 1, "coconuts" = 1, "updated_at" = '...'
WHERE "app_knight"."id" = 1

save(update_fields=["name"]) writes one. The difference matters under concurrency: two processes saving the same instance silently overwrite each other’s unrelated fields.

A QuerySet caches its results, a new QuerySet does not. Checked: list(qs) twice on the same object is 1 query, two freshly built querysets is 2. count() on an unevaluated queryset issues SELECT COUNT(*); on one already evaluated it issues nothing and counts the cache. len(qs) always fetches the rows. So count() when a number is all you want, len() when you were going to iterate anyway.

Catching IntegrityError inside atomic() poisons the block. The next query raises TransactionManagementError: An error occurred in the current transaction. You can't execute queries until the end of the 'atomic' block. The fix is a savepoint, which is what a nested atomic() is:

with transaction.atomic():
    try:
        with transaction.atomic():        # savepoint, rolled back on its own
            Quest.objects.create(name="grail")
    except IntegrityError:
        pass
    Quest.objects.create(name="shrubbery")  # still works

bulk_create() fires no signals. Checked: post_save fired zero times for two objects created in bulk, once for a plain create(). Overridden save() methods are skipped for the same reason. Primary keys do come back on the instances (SQLite and PostgreSQL).

Deleting a queryset loads the rows as soon as something cascades. Checked: deleting a Quest that knights point at runs a SELECT of the quests, then a DELETE of the knights, then a DELETE of the quests. on_delete=CASCADE is enforced by Django, not by the database, which is why signals fire and why memory grows with the number of rows. Deleting a model nothing points at takes the fast path, a single DELETE.

F() after save() changed in 6.0. On 5.2.18, the attribute is still a CombinedExpression after the save and int(k.coconuts) raises TypeError, hence the old habit of calling refresh_from_db(). On 6.0.9 and 6.1.2 it is the resolved value. From the 6.0 release notes: fields assigned expressions are refreshed after save() on backends that support the RETURNING clause (SQLite, PostgreSQL, Oracle); on MySQL and MariaDB they are marked deferred, so the refresh happens on first access instead.

Async mode is a query API, not an async data layer

Every QuerySet method that issues SQL has an a-prefixed variant, async for works on all querysets, and model methods like asave() and acreate() exist. That is real, and it is also the whole of it.

The documentation is blunt about the limit: “Transactions do not yet work in async mode.” The recommendation is to “write that piece as a single synchronous function and call it using sync_to_async()”.

No async transactions means no async unit of work, so every write path that needs atomicity still funnels through a thread. If your handler does more than one related write, the async version buys you nothing and costs you a thread hop.

The second limit is easy to miss: persistent connections must be turned off. “Persistent database connections, set via the CONN_MAX_AGE setting, should also be disabled in async mode. Instead, use your database backend’s built-in connection pooling if available.” Leaving the synchronous default in place is a slow, hard-to-attribute failure under load rather than an error.

What that looks like in practice, checked on 6.1.2:

async with transaction.atomic():   # TypeError: 'Atomic' object does not support
    ...                            # the asynchronous context manager protocol

Knight.objects.count()             # SynchronousOnlyOperation: You cannot call this
                                   # from an async context - use a thread or sync_to_async

The TypeError wording depends on the Python version; 3.15 adds “Did you mean to use ‘with’?”. The second one is the one that bites, because it fires from any synchronous ORM call reached indirectly from an async view, a serializer or a template. A plain with transaction.atomic(): inside an async function raises it too, on entry, before anything inside the block runs. Wrap the whole transactional unit in a sync function and call it with sync_to_async.

Breaking changes in 6.0 and 6.1

Both quoted from the release notes, because both are the silent kind.

Field.pre_save() must now be idempotent. From the 6.0 notes: “Field.pre_save() may now be called more than once when saving model instances, so custom implementations should be idempotent and free of side effects.” Any custom field that incremented a counter, wrote a log line or generated a token in pre_save now does it twice.

first() and last() changed which row they return. From the 6.1 notes: “first() and last() no longer order by the primary key when a QuerySet’s ordering has been forcibly cleared by calling order_by() with no arguments.” No exception, no warning - a different row. Checked on 6.1.2: Knight.objects.order_by().first() emits LIMIT 1 with no ORDER BY clause at all, where Knight.objects.first() still emits ORDER BY "app_knight"."id" ASC LIMIT 1.

Two deprecations in 6.1 that hit ordinary code. Calling select_related() with no arguments is deprecated (specify the related fields, or use FETCH_PEERS below), and so is values_list(flat=True) with no field name (pass one, like values_list("pk", flat=True)). Both raise a pending-deprecation warning today and still work: RemovedInDjango2028Warning since 6.1.2, which renamed RemovedInDjango70Warning (kept as an alias) as Django moves to calendar versioning. Run the test suite with -W error to find them before the upgrade does.

DEFAULT_AUTO_FIELD now defaults to BigAutoField in 6.0. Most projects are unaffected, since Django has raised the models.W042 check for this since 3.2. If you never dealt with that warning, set DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' explicitly before upgrading.

Django 6.0 supports Python 3.12, 3.13 and 3.14, and officially supports only the latest release of each series. Django does not ship py.typed; type checking still needs django-stubs.

Field fetch modes, for N+1

New in 6.1, and the structural answer to deferred-field N+1 queries. Three modes: FETCH_ONE (the default, existing behaviour, fetches the missing field for the current instance only), FETCH_PEERS (fetches it for every instance from the same QuerySet), and FETCH_RAISE (raises FieldFetchBlocked).

Measured on the same three knights, with coconuts left out of the initial query:

for k in Knight.objects.only("name"):                                  # 4 queries
    k.coconuts
for k in Knight.objects.only("name").fetch_mode(models.FETCH_PEERS):   # 2 queries
    k.coconuts
for k in Knight.objects.only("name").fetch_mode(models.FETCH_RAISE):
    k.coconuts   # FieldFetchBlocked: Fetching of Knight.coconuts blocked.

FETCH_RAISE in tests is the useful one: it turns an accidental N+1 into a failure instead of a slow endpoint.

Settings and model snippets

Created and updated timestamps on a model:

class MyTimestampedModel(models.Model):
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

Email settings:

EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_DEFAULT_FROM = '...'
EMAIL_HOST = 'smtp.example.com'
EMAIL_HOST_USER = '...'
EMAIL_HOST_PASSWORD = '...'
EMAIL_USE_TLS = True
EMAIL_PORT = 587

Applications

  • Django Filter - queryset filtering from query parameters. Active (26.2, October 2026).
  • Django Crispy Forms - form rendering. Active (2.7, July 2026).
  • Django Braces - class-based view mixins. Quiet (1.17.0, March 2025).
  • Django Vanilla Views - simplified class-based views. Dormant: last release 3.0.0 in May 2021, last commit September 2022, and its own site has no valid HTTPS.

Related

Keyboard shortcuts

/Open search
CtrlKOpen search (Cmd+K on macOS)
?Show this cheat sheet
Ctrl/Show this cheat sheet (Cmd+/ on macOS)
EscClose search or this cheat sheet

Turns off the single-key shortcuts (/, ?). Ctrl/Cmd+K and Ctrl/Cmd+/ stay.