python 42 lines · 10 steps

How soft deletes work in Django

A reusable abstract model that marks rows deleted with a timestamp instead of removing them.

Explained by highlit
1from django.db import models
2from django.utils import timezone
3 
4 
5class SoftDeleteQuerySet(models.QuerySet):
6 def delete(self):
7 return super().update(deleted_at=timezone.now())
8 
9 def hard_delete(self):
10 return super().delete()
11 
12 def alive(self):
13 return self.filter(deleted_at__isnull=True)
14 
15 def dead(self):
16 return self.filter(deleted_at__isnull=False)
17 
18 
19class SoftDeleteManager(models.Manager):
20 def get_queryset(self):
21 return SoftDeleteQuerySet(self.model, using=self._db).alive()
22 
23 
24class SoftDeleteModel(models.Model):
25 deleted_at = models.DateTimeField(null=True, blank=True, editable=False)
26 
27 objects = SoftDeleteManager()
28 all_objects = SoftDeleteQuerySet.as_manager()
29 
30 class Meta:
31 abstract = True
32 
33 def delete(self, using=None, keep_parents=False):
34 self.deleted_at = timezone.now()
35 self.save(using=using, update_fields=["deleted_at"])
36 
37 def hard_delete(self, using=None, keep_parents=False):
38 super().delete(using=using, keep_parents=keep_parents)
39 
40 def restore(self):
41 self.deleted_at = None
42 self.save(update_fields=["deleted_at"])
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Overriding delete to set a timestamp keeps rows recoverable while hiding them from normal queries.
  2. 2A custom manager that filters in get_queryset makes soft-deleted rows invisible by default everywhere.
  3. 3Pairing a default manager with an unfiltered one lets you reach hidden rows when you genuinely need them.

Related explainers

Share this explainer

Here's the card — post it anywhere.

How soft deletes work in Django — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code