Django Permanent
Yet another approach to provide soft (logical) delete or masking (thrashing) django models instead of deleting them physically from db.
Installation
Install using pip:
pip install django-permanent
Or install from source:
git clone https://github.com/meteozond/django-permanent.git
cd django-permanent
python setup.py install
Requirements:
- Python 3.10+
- Django 4.2+
Quick Start
To create a non-deletable model just inherit it from PermanentModel:
class MyModel(PermanentModel):
pass
It automatically changes delete behaviour to hide objects instead of deleting them:
>>> a = MyModel.objects.create(pk=1)
>>> b = MyModel.objects.create(pk=2)
>>> MyModel.objects.count()
2
>>> a.delete()
>>> MyModel.objects.count()
1
To recover a deleted object just call its restore method:
>>> a.restore()
>>> MyModel.objects.count()
2
Use the force kwarg to enforce physical deletion:
>>> a.delete(force=True) # Will act as the default django delete
>>> MyModel._base_manager.count()
0
Restore on Create
If you want create() to restore deleted objects instead of raising an integrity error on unique constraints, use the restore_on_create option:
class Article(PermanentModel):
title = models.CharField(max_length=100, unique=True)
class Permanent:
restore_on_create = True
How it works:
When restore_on_create = True, calling Model.objects.create(**kwargs) will:
- First try to find a matching object (including soft-deleted ones)
- If found and deleted: restore it and update with new kwargs
- If found and not deleted: return the existing object
- If not found: create a new object
Example:
>>> article = Article.objects.create(title="Django Tips")
>>> article.delete() # Soft delete
>>> Article.objects.count()
0
# Without restore_on_create: would raise IntegrityError
# With restore_on_create: restores the deleted article
>>> article2 = Article.objects.create(title="Django Tips")
>>> article2.pk == article.pk # Same object!
True
>>> Article.objects.count()
1
Note: This feature is most useful for models with unique constraints where you want to "resurrect" deleted objects rather than creating duplicates.
Managers
It changes the default model manager to ignore deleted objects, adding a deleted_objects manager to see them instead:
>>> MyModel.objects.count()
2
>>> a.delete()
>>> MyModel.objects.count()
1
>>> MyModel.deleted_objects.count()
1
>>> MyModel.all_objects.count()
2
>>> MyModel._base_manager.count()
2
Accessing Deleted Related Objects
By default, accessing a foreign key to a deleted object will raise DoesNotExist. Use show_all_context() to access deleted related objects:
from django_permanent.related import show_all_context
# Create models with relationship
parent = ParentModel.objects.create(name="parent")
child = ChildModel.objects.create(parent=parent)
# Soft delete parent
parent.delete()
# Without show_all_context: raises DoesNotExist
child.parent # Raises ParentModel.DoesNotExist
# With show_all_context: can access deleted parent
with show_all_context():
print(child.parent.name) # Works! Returns "parent"
Note: This is useful when you need to access relationships to soft-deleted objects, for example in admin interfaces or audit logs.
QuerySet
The QuerySet.delete method will act as the default django delete, with one exception - objects of models subclassing PermanentModel will be marked as deleted; the rest will be deleted physically:
>>> MyModel.objects.all().delete()
You can still force django query set physical deletion:
>>> MyModel.objects.all().delete(force=True)
Using custom querysets
-
Inherit your query set from
PermanentQuerySet:class ServerFileQuerySet(PermanentQuerySet) pass
-
Wrap
PermanentQuerySetorDeletedQuerySetin you model manager declaration:class MyModel(PermanentModel) objects = MultiPassThroughManager(ServerFileQuerySet, NonDeletedQuerySet) deleted_objects = MultiPassThroughManager(ServerFileQuerySet, DeletedQuerySet) all_objects = MultiPassThroughManager(ServerFileQuerySet, PermanentQuerySet)
Method get_restore_or_create
- Check for existence of the object.
- Restore it if it was deleted.
- Create a new one, if it was never created.
Field name
The default field named is 'removed', but you can override it with the PERMANENT_FIELD variable in settings.py:
PERMANENT_FIELD = 'deleted'
Requirements
- Django 4.2+
- Python 3.10, 3.11, 3.12+
Testing
The project uses GitHub Actions for continuous integration.
Run tests locally using act (GitHub Actions locally):
# Install act (macOS)
brew install act
# Run all tests in parallel
act
# Run specific Python version
act --matrix python-version:3.11
# Run specific Python/Django combination
act --matrix python-version:3.11 --matrix django-version:"Django>=4.2,<5.0"
Run tests directly:
# Install dependencies
pip install "Django>=4.2,<5.0" coverage flake8
# Run linter
flake8 django_permanent
# Run tests with coverage
coverage run runtests.py
coverage report
Metadata
Release files for django-permanent 2.0.0
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 | |
|---|---|---|---|---|
| django_permanent-2.0.0.tar.gz | 20.4 kB | With provenance | Details |
Release files / django_permanent-2.0.0.tar.gz
| Download URL | django_permanent-2.0.0.tar.gz |
|---|---|
| Size | 20.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
671fc0b30b07fb0d781014f8173e8191bf26a8bbfb8990b2685e5610c54e899a
|
|
BLAKE2b-256 checksum How to use checksums |
5ad252794af389feb0e01cb9f5008c2b7f71d310937073200629727d67978d52
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Feb 7, 2026.
Transparency logSecurity for django-permanent 2.0.0
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.
Full provenance
All 1 file in this release has been uploaded with provenance metadata.
Attestation for django_permanent-2.0.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Feb 7, 2026.
Transparency logA 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.