PYTHON / DATABASES WITH PYTHON
CRUD operations in MongoDB
Insert, read, update, and delete MongoDB documents from Python with PyMongo, and read the result objects each write returns.
What you will learn
- Write documents with insert_one/insert_many and use the returned inserted ids
- Read with find_one and find using filter documents and projections
- Change fields with $set/$inc instead of replacing whole documents
- Distinguish matched_count, modified_count, upserted_id, and deleted_count
Understanding CRUD operations in MongoDB
Every PyMongo write method takes a filter document and, for updates, a second document describing the change. The filter is not a string to parse: it is a dict where each key is a field path and each value is either a literal to match exactly or an operator document such as {"$lt": 1990}. Because the filter and the update are separate arguments, MongoDB never has to guess where your data ends and your query begins, which is why PyMongo has no equivalent of SQL injection through string building.
The single most confusing part of updating is that {"$set": {"copies": 3}} and {"copies": 3} mean completely different things. An update document made of operators patches the fields you name and leaves the rest of the document alone; a plain document with no operators is a full replacement, and PyMongo refuses it in update_one/update_many so you cannot silently wipe fields. If you really want to swap the whole document, call replace_one, which accepts a plain document and keeps only the _id.
Writes report what happened through small result objects rather than return values you have to infer. update_one gives you matched_count (how many documents the filter found), modified_count (how many actually changed on disk), and upserted_id (set only when upsert=True created a new document). matched_count of 1 with modified_count of 0 is normal and means the document was already in the requested state, so treating modified_count as "did my filter work" is a common source of false alarms.
from pymongo import MongoClient
client = MongoClient("mongodb://localhost:27017/")
books = client["lesson_db"]["books"]
books.drop()
books.insert_many([
{"title": "Dune", "year": 1965, "copies": 4},
{"title": "Neuromancer", "year": 1984, "copies": 2},
{"title": "Snow Crash", "year": 1992, "copies": 0},
])
print(books.find_one({"title": "Dune"}, {"_id": 0}))
res = books.update_one({"title": "Snow Crash"}, {"$set": {"copies": 3}})
print(res.matched_count, res.modified_count)
res = books.update_many({"year": {"$lt": 1990}}, {"$inc": {"copies": 1}})
print(res.matched_count, res.modified_count)
for row in books.find({"copies": {"$gte": 3}}, {"_id": 0, "title": 1}).sort("title"):
print(row)
res = books.delete_many({"year": {"$lt": 1980}})
print(res.deleted_count, books.count_documents({}))
client.close()In MongoDB the filter says which documents to touch and the update operators say how to change them, and the returned result object tells you exactly what happened.
Worked examples
Upsert, unchanged updates, and full replacement
Shows how upsert creates a missing document, why modified_count can be 0, and how replace_one drops unlisted fields.
from pymongo import MongoClient
col = MongoClient("mongodb://localhost:27017/")["lesson_db"]["stock"]
col.drop()
r = col.update_one({"sku": "A1"}, {"$set": {"qty": 10}}, upsert=True)
print("matched", r.matched_count, "modified", r.modified_count, "created", r.upserted_id is not None)
r = col.update_one({"sku": "A1"}, {"$set": {"qty": 10}}, upsert=True)
print("matched", r.matched_count, "modified", r.modified_count, "created", r.upserted_id is not None)
col.replace_one({"sku": "A1"}, {"sku": "A1", "warehouse": "east"})
print(col.find_one({"sku": "A1"}, {"_id": 0}))Example explained
Line 1The first update matches nothing, so upsert=True inserts a document built from the filter plus the $set fields and reports its new _id in upserted_id.
Line 2The second call matches that document but qty is already 10, so MongoDB writes nothing and modified_count stays 0 while matched_count becomes 1.
Line 3replace_one accepts a plain document with no operators and swaps the whole body, so qty disappears and only sku and warehouse remain.
Line 4The projection {"_id": 0} hides the ObjectId, which is why the printed dict is stable across runs.
Read and modify in one atomic call
Uses find_one_and_update and find_one_and_delete to get the document back instead of only a count.
from pymongo import MongoClient, ReturnDocument
col = MongoClient("mongodb://localhost:27017/")["lesson_db"]["counters"]
col.drop()
col.insert_one({"_id": "invoice", "seq": 41})
after = col.find_one_and_update(
{"_id": "invoice"},
{"$inc": {"seq": 1}},
return_document=ReturnDocument.AFTER,
)
print(after)
before = col.find_one_and_update(
{"_id": "invoice"},
{"$inc": {"seq": 1}},
return_document=ReturnDocument.BEFORE,
)
print(before, col.find_one({"_id": "invoice"})["seq"])
print(col.find_one_and_delete({"_id": "invoice"}))
print(col.count_documents({}))Example explained
Line 1_id can be any unique value, here the string "invoice", so the counter document is addressable without an ObjectId.
Line 2$inc adds to the stored number server-side, so no read-modify-write race is possible between two clients.
Line 3return_document=ReturnDocument.AFTER returns the incremented document (42); the default BEFORE returns the pre-update state, which is why the second call prints 42 while the stored value is already 43.
Line 4find_one_and_delete removes the document and hands it back, so you can log or reuse it before it is gone.
Important notes
insert_one mutates the dict you pass by adding an "_id" key, so reusing the same dict object for a second insert raises DuplicateKeyError.
update_many is not a transaction: each document is updated atomically on its own, and a failure partway through leaves earlier documents already changed.
Common mistakes
Calling update_one({"title": "Dune"}, {"copies": 3}) without $set: PyMongo raises ValueError because a bare document is a replacement, not a patch, and the intended field update never happens.
Treating modified_count == 0 as a failed filter and retrying or raising an error, when it usually just means the document already held that value.
Using delete_one or update_one when several documents match: MongoDB picks one matching document and the rest are left untouched, so the data ends up half-changed with no error.
Try it yourself
Change, predict, then run
Create a collection of three task documents with fields title, done, and priority, then use update_many to set done to True for every task with priority above 2, and print matched_count, modified_count, and the number of documents still not done.
Open the Python workspaceCheck your understanding
An update_one call returns matched_count 1 and modified_count 0. What does this tell you?
- The filter found a document, but its fields already had the values you were setting
- The filter found nothing, so MongoDB fell back to doing no work
- The update was rejected because it lacked an operator such as $set
- The write reached the server but has not been applied yet and needs a commit
Show answer
matched_count counts documents the filter selected and modified_count counts documents whose stored bytes actually changed, so 1 and 0 means a document matched and was already in the requested state. "The filter found nothing" is tempting but would report matched_count 0, and a missing operator would raise ValueError in PyMongo rather than return a result object.