Chapter 14: Django Delete Data
1. The Four Main Ways to Delete Data (Quick Comparison Table)
| Method | What it does | Speed (relative) | Affects how many? | Calls model .delete()? | Signals? (pre/post_delete) | Cascades relationships? | Best for |
|---|---|---|---|---|---|---|---|
| instance.delete() | Delete one object | Medium | 1 | Yes | Yes | Yes (follows on_delete) | Normal CRUD, views, forms |
| QuerySet.delete() | Bulk DELETE – fastest SQL way | Very fast (1 query) | Many | No | Yes (but per-object) | Yes | Mass cleanup, admin actions |
| bulk_delete() (not built-in) | Custom or third-party | Fast | Many | No | Optional | Depends | Rare – usually use QuerySet.delete() |
| Soft delete (custom field) | Mark as deleted (is_deleted=True) | Fast | 1 or many | No | Custom signals | No real delete | Production apps (audit, undo) |
Django 6.0.1 note: No major changes to deletion since 4.x/5.x — QuerySet.delete() still emits pre_delete/post_delete signals for each object (can be slow on large sets), and cascading still respects on_delete.
2. Method 1: Delete One Object – instance.delete()
Most common in views/admin.
In shell (python manage.py shell):
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
from polls.models import Question, Choice # Get a question (assume pk=1 exists) q = Question.objects.get(pk=1) print(q) # → "What's your favorite biryani...?" # Delete it deleted_count, details = q.delete() print(deleted_count) # → 1 (questions) + number of related choices print(details) # → {'polls.Question': 1, 'polls.Choice': 3} |
What really happens?
- Django collects all related objects (follows ForeignKey/ManyToMany)
- For each related object → calls .delete() recursively (if CASCADE)
- Emits pre_delete signal for each object
- Executes SQL DELETEs
- Emits post_delete signal for each
Because we used on_delete=models.CASCADE on Choice → deleting Question also deletes all its Choices automatically.
In a view example (e.g. delete poll):
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
from django.shortcuts import get_object_or_404, redirect def delete_question(request, question_id): question = get_object_or_404(Question, pk=question_id) if request.method == 'POST': # confirm delete question.delete() # cascades to choices return redirect('polls:index') return render(request, 'polls/delete_confirm.html', {'question': question}) |
Add CSRF + confirmation template in real code!
3. Method 2: Bulk Delete – QuerySet.delete() (Fast & Dangerous!)
One SQL statement — no per-object .delete() calls.
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
# Delete all old questions (older than 1 year) from datetime import timedelta from django.utils import timezone old_questions = Question.objects.filter( pub_date__lt=timezone.now() - timedelta(days=365) ) deleted_count, details = old_questions.delete() print(deleted_count) # total rows deleted print(details) # {'polls.Question': 42, 'polls.Choice': 150} |
Important behaviors:
- Still cascades → deletes related objects according to on_delete
- Emits pre_delete and post_delete signals for each object (can be slow if 10,000+ objects!)
- Does not call custom .delete() overrides on models
When to prefer this: Admin actions, cleanup scripts, management commands.
Danger: Accidentally Question.objects.all().delete() → wipes entire table + cascades everywhere!
Always filter + .count() first:
|
0 1 2 3 4 5 6 7 8 |
qs = Question.objects.filter(status='draft') print(qs.count()) # check before delete qs.delete() |
4. on_delete Options – What Happens When Parent is Deleted?
This is critical — controls child behavior when parent deleted.
In your Choice model:
|
0 1 2 3 4 5 6 7 8 9 10 |
question = models.ForeignKey( Question, on_delete=..., # ← this decides! ... ) |
Common options (all from django.db.models):
- models.CASCADE (default) → Delete parent → auto-delete all children Example: Delete Question → all its Choices gone
- models.PROTECT → Raise ProtectedError if children exist
Python0123456question = models.ForeignKey(..., on_delete=models.PROTECT)
→ Try q.delete() → error if choices exist → safe for important relations
- models.SET_NULL → Set child FK to NULL (field must have null=True)
Python0123456question = models.ForeignKey(..., on_delete=models.SET_NULL, null=True)
→ Delete Question → Choice.question becomes None (keeps choices)
- models.SET_DEFAULT → Set to default value (field must have default=…)
- models.DO_NOTHING → Do nothing → database error if constraint violation
- models.SET(value_or_callable) → Set to specific value or callable result
2026 best practice: Use PROTECT for most user-generated content (prevent accidental wipes), CASCADE only when children are useless without parent (comments without post), SET_NULL for audit/history.
5. Soft Delete – Very Popular in Production (Don’t Really Delete!)
Instead of hard delete → add field + filter everywhere.
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
# Add to models.py class Question(models.Model): ... is_deleted = models.BooleanField(default=False) deleted_at = models.DateTimeField(null=True, blank=True) def delete(self, *args, **kwargs): self.is_deleted = True self.deleted_at = timezone.now() self.save(update_fields=['is_deleted', 'deleted_at']) # Or raise exception if you want to block hard delete |
Then override manager:
|
0 1 2 3 4 5 6 7 8 9 10 11 |
class QuestionManager(models.Manager): def get_queryset(self): return super().get_queryset().filter(is_deleted=False) # In model objects = QuestionManager() |
Now Question.objects.all() excludes deleted ones automatically.
Use all_objects for admin if needed.
Pros: Undo possible, audit trail, no cascade surprises Cons: DB grows, queries need care
Many 2026 projects (especially SaaS) use soft delete + celery cleanup.
6. Common Mistakes & Real Fixes
- Cascade deleted too much → Forgot to check on_delete → use PROTECT in dev
- Slow bulk delete → Signals per object → use raw SQL or transaction.atomic() + disable signals temporarily (advanced)
- IntegrityError on delete → DO_NOTHING without DB cascade → change to CASCADE/PROTECT
- Deleted wrong records → No confirmation → always use POST + confirm page in views
Quick Exercise
- Create 2 questions + 3 choices each
- Delete one question → see choices gone (CASCADE)
- Change to on_delete=models.PROTECT → migrate → try delete → error
- Try bulk Question.objects.filter(question_text__contains=”biryani”).delete()
Tell me:
- “Worked! Show soft delete implementation in detail”
- “How to delete with confirmation in view/template?”
- “What if I want to delete without cascade?”
- “Error ProtectedError — how to handle?”
- Or next topic: “Django Forms” / “Authentication” / “Full polls voting”
You’re handling the full CRUD now — super solid foundation! 💪🚀 Let’s keep building! 🇮🇳
