Skip to main content

DuckDI Logo

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_at field automatically added to your models;
  • Ready-to-use managers:
    • objects → filters out deleted records;
    • everything → returns all records (including deleted);
  • soft_delete() and restore() 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

SoftDeleteAdmin provides:

  • filter SoftDeletedAtFilter (Not Deleted / Deleted);
  • actions “Soft delete selected” and “Restore selected”;
  • get_queryset using everything so 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 keep delete() 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;
  • get_queryset uses model.everything.all() so admin shows all records.

Tip: you can remove Django’s default delete_selected action to avoid accidental hard deletes.


Tests

See the tests/ folder in this repo (pytest + pytest-django), covering:

  • Managers (objects vs everything);
  • soft_delete() and restore();
  • 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)

Source distribution for duck_django_soft_delete 0.1.7
File Size Uploaded Provenance
duck_django_soft_delete-0.1.7.tar.gz 4.9 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for duck_django_soft_delete 0.1.7
File Interpreter ABI Platform
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.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page