Deleting Documents
Deletion in MongoEngine has two entry points - an instance's delete() and a queryset's delete() - and one strategic question that matters far more than the syntax: hard delete, or soft delete? The syntax is two lines; the strategy is how you keep history, support undo, and avoid the cascade problems that unlinked data causes. This chapter covers both.
Deleting a Single Document
post = Post.objects.get(slug="hello")
post.delete() # removes the document from the collection
# gone for good - this now raises DoesNotExist
Post.objects.get(id=post.id)
Instance delete removes exactly that document by its _id. It runs any pre_delete signals (the signals chapter), and it is the right tool when you hold the instance and want the full delete lifecycle. Note it does not touch documents that reference it - references are not automatically cleaned, which is the reason soft deletes and explicit cascade handling exist.
Deleting a Cohort
# returns the number of documents removed
n = Post.objects(draft=True, created_at__lt=cutoff).delete()
print("deleted:", n)
# a queryset delete is one server-side operation for the whole match set
Post.objects(tags__contains="expired").delete()
Queryset delete matches, removes everything that matches, and returns the count - one round trip, no per-document load. It is the standard tool for maintenance jobs (purge old drafts, clear a test dataset, expire a batch), and the returned count belongs in the job's log: "deleted 1,204 documents" is the line that makes a bulk delete auditable.
Soft Deletes
from mongoengine import Document, BooleanField, DateTimeField
class Post(Document):
title = StringField()
deleted_at = DateTimeField(default=None)
meta = {"indexes": ["deleted_at"]}
@classmethod
def active(cls):
return cls.objects(deleted_at=None)
def soft_delete(self):
type(self).objects(id=self.id).update(set__deleted_at=datetime.datetime.now(datetime.timezone.utc))
def restore(self):
type(self).objects(id=self.id).update(unset__deleted_at=True)
The pattern: a deleted_at timestamp, a scoped queryset (active()) that every read path uses, and a soft_delete() that is an atomic update rather than a removal. The document keeps its _id and its relationships stay intact, undo is a field update, and analytics can distinguish "never existed" from "existed and was removed". The discipline that makes it work: every query goes through the scoped queryset, so undeleted documents are the visible set by default, not by accident.
What Happens to Data That Points at It
- Embedded documents live inside their parent - deleting the parent deletes them, automatically and atomically. There is nothing to cascade.
- ReferenceField values are bare ObjectIds - they survive their target's deletion and become
dereferenced()toNone(or raise, depending on configuration). The owning document must handle the missing target. - The practical rule: before deleting a parent, either delete or re-point the children explicitly (a short loop over the child queryset), or prefer soft delete where the parent has dependents.
Dropping Collections (Maintenance)
Post.drop_collection() # removes the entire collection
# the ODM wrapper for tests and migrations
Post.drop_collection()
Post.ensure_indexes() # recreate the index set
drop_collection() is a maintenance operation - wiping a test database, resetting a collection after a schema change - not an application delete. It removes documents, indexes, and the collection itself, so it belongs in scripts and fixtures, not in request handlers.
Summary
Delete a single document with instance.delete(), a cohort with queryset.delete() (logging the returned count), and a whole collection with drop_collection() in maintenance code. Where history, undo, or dependents matter, use a soft delete - a deleted_at field plus a scoped queryset on every read path. Remember that references do not cascade: embedded children die with their parent, but reference fields become dangling ObjectIds, so decide the dependent-data policy before the first delete().