duck-django-soft-delete
Pragmatic soft delete framework for Django.
It provides a safe and transparent way to "delete" records by marking them with a timestamp instead of permanently removing them from the database.
This approach is useful when you need:
- Data recovery after accidental deletions;
- Historical/audit tracking;
- To keep database integrity while still hiding records from normal queries;
- To allow reusing unique keys (with conditional uniqueness).
Features
deleted_atfield automatically added to your models;- Ready-to-use managers:
objects→ filters out deleted records;everything→ returns all records (including deleted);
soft_delete()andrestore()methods;- Admin integration: filter by deletion status + actions for Soft Delete and Restore;
- Helper
build_prefetches()to automatically prefetch only alive related records; - Supports partial unique constraints in Postgres (unique only among alive records).
Installation
pip install duck-django-soft-delete
Add to your project and import:
# settings.py
INSTALLED_APPS = [
# ...
"duck_django_soft_delete",
]
Concept
Abstract base
from duck_django_soft_delete.table.soft_delete_table import SoftDeleteTable
class MyModel(SoftDeleteTable):
# your fields...
SoftDeleteTable defines:
- Model time stamp fields:
created_at,updated_at - Soft delete model field:
deleted_at - Django, soft delete and restore model methods:
soft_delete()/restore() - Django prefetch_related method for soft delete
build_prefetches(relations: list[str]) -> list[Prefetch|str]
Managers
objects→ alive only (deleted_at IS NULL)everything→ all (alive and deleted)
Real example
Model with conditional uniqueness (Postgres)
from duck_django_soft_delete.table.soft_delete_table import SoftDeleteTable
from django.db import models
from uuid import uuid4
class Item(SoftDeleteTable):
uuid = models.UUIDField(primary_key=True, default=uuid4, editable=False)
name = models.CharField(max_length=255)
description = models.TextField(blank=True, null=True, default="")
is_public = models.BooleanField(default=True)
foreign_key_exemple = models.ForeignKey(
"foreign_key_exemple.ForeignKeyExemple",
on_delete=models.CASCADE,
db_column="foreign_key_exemple_id",
related_name="foreign_key_exemples",
)
class Meta(SoftDeleteTable.Meta):
db_table = "items"
Admin
from duck_django_soft_delete.admin.soft_delete_admin import SoftDeleteAdmin
from django.contrib import admin
from item import Item
@admin.register(Item)
class ItemAdmin(SoftDeleteAdmin):
list_display = (
"uuid",
"name",
"is_public",
"created_at",
"updated_at",
"deleted_at",
)
list_filter = [
*SoftDeleteAdmin.list_filter,
"is_public",
"foreign_key_exemple",
"updated_at",
]
search_fields = ("name", "is_public", "foreign_key_exemple__id", )
# ... your @admin.display helpers
SoftDeleteAdminprovides:
- filter
SoftDeletedAtFilter(Not Deleted / Deleted);- actions “Soft delete selected” and “Restore selected”;
get_querysetusingeverythingso admin sees all records.
Usage
obj = Item.objects.create(name="X")
obj.soft_delete() # sets deleted_at = now()
obj.restore() # clears deleted_at
Item.objects.all() # only alive
Item.everything.all() # all
Prefetch alive-only relations
If you want to prefetch only alive children when the related model also inherits SoftDeleteTable:
qs = Item.objects.all().prefetch_related(
*Item.build_prefetches(["foreign_key_exemples", ])
)
- If the relation inherits
SoftDeleteTable, prefetch uses its alive manager. - If it does not, falls back to standard Django prefetch.
Best practices
- Postgres: use
UniqueConstraint(..., condition=Q(deleted_at__isnull=True))for uniqueness only on alive records.
class Meta(SoftDeleteTable.Meta):
db_table = "items"
constraints = [
models.UniqueConstraint(
fields=["foreign_key_exemple", "name"],
condition=Q(deleted_at__isnull=True),
name="unique_item_name_per_foreign_key_exemple_not_deleted",
)
]
- Document whether you override
Model.delete()to do soft delete by default (this lib does not enforce — you can choose to keepdelete()as hard delete).
Compatibility
- Django: 4.2+
- Python: 3.10+
Admin — details
- Filter: “Soft Delete” → Not Deleted / Deleted
- Actions:
- Soft delete selected: calls
soft_delete()on alive items; - Restore selected: calls
restore()on deleted items;
- Soft delete selected: calls
get_querysetusesmodel.everything.all()so admin shows all records.
Tip: you can remove Django’s default
delete_selectedaction to avoid accidental hard deletes.
Tests
See the tests/ folder in this repo (pytest + pytest-django), covering:
- Managers (
objectsvseverything); soft_delete()andrestore();- Partial unique (Postgres);
- Admin actions;
build_prefetches().
License
MIT
Metadata
Release files for duck_django_soft_delete 0.1.7
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| duck_django_soft_delete-0.1.7.tar.gz | 4.9 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| duck_django_soft_delete-0.1.7-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 11.9 kB
Release files / duck_django_soft_delete-0.1.7.tar.gz
| Download URL | duck_django_soft_delete-0.1.7.tar.gz |
|---|---|
| Size | 4.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9a71db0d2c6325a329e6860bc42bfe0edcd85bfad898148e079e30c0a4fc11d4
|
|
BLAKE2b-256 checksum How to use checksums |
8eb50c5d713951ebb36e03bdea48da686fccbbc1e6ad99e25866254cb5ec343a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
poetry/2.1.3 CPython/3.13.4 Darwin/24.0.0
|
Release files / duck_django_soft_delete-0.1.7-py3-none-any.whl
| Download URL | duck_django_soft_delete-0.1.7-py3-none-any.whl |
|---|---|
| Size | 7.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e5d913b6be623433a01cf8bcfcd00ab8fa36a1a565b64b48e1f9292f626e5472
|
|
BLAKE2b-256 checksum How to use checksums |
4695a8a0abb0c11cee3e88c20b17385c0a13aded4d14b55d4fb5313579c2ea5f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
poetry/2.1.3 CPython/3.13.4 Darwin/24.0.0
|
Security for duck_django_soft_delete 0.1.7
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.