In Django, a WikiPage model generates its slug in an overridden save(); which write paths never run that code?
answer
- SQL-level writes skip Python methods
- update, bulk_create, bulk_update
- fixtures and data migrations
- queryset delete and cascades
basics
~10 sQuerySet.update(), bulk_create(), bulk_update(), raw SQL, loaddata fixtures and data migrations using historical models all skip an overridden save(). QuerySet.delete() and cascades likewise skip an overridden delete().
solid answer
~40 sAn overridden `save()` runs only when something calls `save()` on an instance: `page.save()`, `WikiPage.objects.create()`, `get_or_create()`/`update_or_create()`, a `ModelForm`'s `save()` and the admin. The bulk paths work at the SQL level and skip it: `QuerySet.update()`, `bulk_create()`, `bulk_update()`, and raw SQL. Fixtures loaded with `loaddata` are written through `save_base(raw=True)`, so they skip it too, and data migrations use historical models that have no custom methods at all. On the delete side, `QuerySet.delete()` and cascades never call an overridden `delete()`. So a slug generated in `save()` is missing for rows created by `bulk_create()`. I keep such logic in a helper called by every path, or move invariants into the database, and write the override as `def save(self, **kwargs)` calling `super().save(**kwargs)`.
code
python · 12 linesfrom django.utils.text import slugify
# Runs WikiPage.save(): slug is filled in
WikiPage.objects.create(title="Getting Started")
# Skips WikiPage.save(): slug stays empty unless set here
WikiPage.objects.bulk_create([
WikiPage(title=t, slug=slugify(t)) for t in ["Install", "Configure"]
])
# Skips WikiPage.save(): title changes, slug does not
WikiPage.objects.filter(pk=7).update(title="Installing on Linux")go deeper
Recall that an overridden save() must call super().save(), and that update() does not call save().
List every path that bypasses save() and delete() overrides, including bulk calls, fixtures and data migrations, and why each one does.
Decide where a rule must live so bulk imports, fixtures and migrations cannot break it, and write overrides that respect update_fields.
Set a team boundary between convenience logic in save() and invariants enforced by the database, so no write path can bypass what matters.
## Where custom save logic lives Overriding `Model.save()` is the standard way to run code whenever an instance is saved. A wiki page is a typical case: the model fills `slug` from `title` so URLs stay readable. ```python from django.db import models from django.utils.text import slugify class WikiPage(models.Model): title = models.CharField(max_length=200) slug = models.SlugField(max_length=220, unique=True) def save(self, **kwargs): if not self.slug: self.slug = slugify(self.title) super().save(**kwargs) ``` The trap is that this code runs **only when `save()` is called on an instance**. Several common write paths never do that. ## Paths that call save() - `page.save()` directly; - `WikiPage.objects.create(...)`, which builds the instance and saves it; - `get_or_create()` and `update_or_create()` when they create or update a row; - `ModelForm.save()` and therefore the admin and generic edit views. ## Paths that skip it | Path | Why it skips `save()` | |---|---| | `QuerySet.update(...)` | one SQL `UPDATE`; no instances are loaded | | `bulk_create([...])` | batches `INSERT` statements without calling each instance's `save()` | | `bulk_update([...], fields)` | batches `UPDATE` statements the same way | | raw SQL / `connection.cursor()` | the ORM is not involved at all | | `loaddata` fixtures | each object is written through `Model.save_base(raw=True)`, bypassing the overridden method | | data migrations (`RunPython`) | historical models are rebuilt from migration state and have no custom methods | Django's docs say it plainly: when creating or updating in bulk, none of `save()`, `pre_save` or `post_save` are called, and there is no workaround. ## The delete side The same applies to `delete()`. `QuerySet.delete()` performs a bulk delete and does not call an overridden `Model.delete()`; neither do cascades triggered by `on_delete`. Deletion signals are still sent in those cases, which is why the docs point to them for delete logic that must always run. Single-instance `page.delete()` does call the override. ## Writing the override correctly 1. **Accept keyword arguments and pass them on**: `def save(self, **kwargs)` and `super().save(**kwargs)`. Django 6.0 removed positional arguments to `save()`, and future keyword arguments keep working when you forward `**kwargs`. 2. **Always call `super().save()`** unless you deliberately block the save; forgetting it means nothing reaches the database. 3. **Add derived fields to `update_fields`** when the caller passed it: if `title` is being saved and you recompute `slug`, include `slug`, or the new slug stays in memory only. 4. **Keep it fast and local**: no network calls, no heavy queries. Every admin save and form submission pays for them. ## How the bypass shows up in production A typical incident: an import command reads a thousand pages from an old system and calls `bulk_create()` for speed. The overridden `save()` never runs, so every row gets the field's empty default for `slug`. Because `slug` is `unique=True`, the first row with an empty slug is inserted and the second collides with it, and the batch fails with an `IntegrityError` for a duplicate key. If `slug` were not unique, the import would succeed and the site would later show a thousand pages whose URLs cannot be reversed. The fix is to compute the slug in the import code (or in a shared helper) before building the objects, and to keep the unique constraint as the safety net that turned a silent data problem into a loud failure. ## Making the rule hold everywhere Once you know which paths skip the override, decide how important the rule is: - **Convenience defaults** (a slug) can live in `save()` plus a shared helper, `WikiPage.build_slug(title)`, that bulk import code calls explicitly before `bulk_create()`. - **Invariants that must never break** (uniqueness, value ranges) belong in the database as constraints, which every writer meets. - **Values the database can compute** can use database defaults or generated columns instead of Python code. - **Data migrations** must copy the logic into the migration, because the historical model cannot call it. An interviewer asking this question wants the list of bypassing paths and the judgment to keep critical rules out of `save()` alone.
- Why does a Django data migration that creates WikiPage rows end up with empty slugs?Inside `RunPython`, `apps.get_model("wiki", "WikiPage")` returns a historical model rebuilt from migration state. It has the fields and `Meta` options but none of your custom methods, so the overridden `save()` never runs. Copy the slug logic into the migration function, which also freezes it against later changes to the model code.
- In Django, does calling QuerySet.delete() on pages run an overridden WikiPage.delete()?No. `QuerySet.delete()` collects the rows and deletes them in bulk without calling each instance's `delete()`, and cascades behave the same. Only `page.delete()` on one instance calls the override. Deletion signals are still sent for the collected rows, which is why the docs point to them for delete logic that must run on every path.
saying these in an interview costs you the question
- QuerySet.update() calls save() on every matched instance.
- bulk_create() runs each object's overridden save() method.
- Loading a fixture with loaddata runs the model's custom save().
- Historical models in migrations include the model's custom methods.
- QuerySet.delete() calls the overridden delete() for each row.