mrkeyoor.com_
Thu 06 Aug 05:55 UTC
PyPIDataupdated 06 Aug 2026

pymongo

PyMongo is MongoDB's own Python driver, and the distribution actually ships three packages: pymongo (the driver), bson (the binary document format MongoDB speaks on the wire), and gridfs (chunked storage for files larger than the 16MB document limit). You create a MongoClient, index into it to get a database and a collection, and then call methods like insert_one, find, update_many and aggregate that map almost one-to-one onto MongoDB commands. Documents go in and come out as plain Python dicts. Since 4.9 the same package also ships AsyncMongoClient, an asyncio API with the same method names, which is what replaced Motor.

Verdict

The only sensible way to reach MongoDB from Python, maintained by the same company that ships the server, and now covering both sync and async in one package. It is a driver and nothing more, so bring your own validation layer if you want typed documents.

API stability4/5Semantic versioning is followed and the 4.x surface has been additive for years, with AsyncMongoClient added in 4.9 without disturbing the sync API. The one point off is the 3.x to 4.0 break, which removed save, insert, update, remove and count outright.
Docs4/5There are two full sites: the MongoDB docs portal for tutorials and the readthedocs API reference with a per-release changelog. The split is the annoyance, since search often lands you on whichever one does not have what you need, and the README spends more space on how to file a JIRA ticket than on usage.
Maintenance5/5Maintained by MongoDB with a paid team, pushed to main the day before this review, 4.17.0 current, and support matrices published for server versions 4.0 through 8.0.
Ecosystem5/5Every Python MongoDB tool sits on top of it: beanie, mongoengine, the deprecated Motor, pymongo-arrow, mongomock for tests, and the Atlas tooling. Roughly 27.7M weekly downloads makes it one of the most installed database drivers in Python.

Use it if

  • You are talking to MongoDB or Atlas from Python and want the driver the server team maintains alongside the database itself
  • You want a thin mapping over MongoDB commands rather than an ORM layer inventing its own query language on top
  • You need asyncio: AsyncMongoClient ships in the same package with the same method names, and Motor is now deprecated in favour of it
  • You need driver features that only the official client implements, such as client-side field level encryption, change streams, GridFS, or MONGODB-AWS and Kerberos auth
  • You want documents as plain dicts so you can hand them to pandas, Pydantic, or a JSON serializer without unwrapping model objects
Skip it if

Setup reality

python -m pip install pymongo is the whole install and dnspython comes with it, so mongodb+srv:// Atlas URIs work out of the box. Two traps follow. First, never pip install bson: PyMongo bundles its own bson package and the unrelated PyPI package of that name shadows it and breaks imports in ways that look like driver bugs. Second, most of the interesting auth and compression paths are extras you have to ask for by name: pymongo[aws] for MONGODB-AWS, pymongo[gssapi] for Kerberos, pymongo[ocsp] for certificate revocation checking, pymongo[snappy] and pymongo[zstd] for wire compression, and pymongo[encryption] for client-side field level encryption, which additionally needs the mongocryptd binary or the shared crypt library on the host. Also remember that MongoClient() does not connect: it returns immediately and the first real network error surfaces 30 seconds later on your first operation, because serverSelectionTimeoutMS defaults to 30000.

Patterns

Connect and insert a documentconnect-and-insert

from pymongo import MongoClient

client = MongoClient("mongodb://localhost:27017")
db = client["shop"]
orders = db["orders"]

result = orders.insert_one({"sku": "ABC", "qty": 3})
print(result.inserted_id)   # ObjectId(...)

MongoClient() does no I/O; it starts background monitoring and returns. Create exactly one client per process and share it, and create it after any fork, because the pool is not fork-safe.

Fail fast instead of waiting 30 secondsverify-connection

from pymongo import MongoClient
from pymongo.errors import ServerSelectionTimeoutError

client = MongoClient(uri, serverSelectionTimeoutMS=3000)
try:
    client.admin.command("ping")
except ServerSelectionTimeoutError as exc:
    raise SystemExit(f"cannot reach mongo: {exc}")

serverSelectionTimeoutMS defaults to 30000, so a bad host or firewall rule looks like a 30 second hang on your first query rather than an immediate error. Lower it for health checks and startup probes.

Find documents with projection, sort and limitquery-documents

import pymongo

doc = orders.find_one({"sku": "ABC"})

cursor = (
    orders.find({"qty": {"$gte": 2}}, {"sku": 1, "qty": 1, "_id": 0})
          .sort("qty", pymongo.DESCENDING)
          .limit(20)
)
for doc in cursor:
    print(doc)

find() returns a lazy cursor, so nothing is sent until you iterate. A cursor can only be consumed once; wrap it in list() if you need to walk the results twice.

Update documents and upsertupdate-and-upsert

res = orders.update_one(
    {"sku": "ABC"},
    {"$set": {"qty": 10}, "$currentDate": {"updated_at": True}},
    upsert=True,
)
print(res.matched_count, res.modified_count, res.upserted_id)

orders.update_many({"status": "new"}, {"$set": {"status": "queued"}})

The second argument must contain update operators. Passing a bare dict like {"qty": 10} raises in PyMongo 4; if you really want whole-document replacement use replace_one.

Batch mixed writes in one round tripbulk-write

from pymongo import InsertOne, UpdateOne, DeleteOne

ops = [
    InsertOne({"sku": "NEW", "qty": 1}),
    UpdateOne({"sku": "ABC"}, {"$inc": {"qty": 1}}),
    DeleteOne({"sku": "OLD"}),
]
result = orders.bulk_write(ops, ordered=False)
print(result.bulk_api_result)

ordered=False lets the server keep going after a failed operation and run the rest in parallel; the default ordered=True stops at the first error. Either way, check result rather than assuming everything applied.

Run an aggregationaggregation-pipeline

pipeline = [
    {"$match": {"status": "shipped"}},
    {"$group": {"_id": "$sku", "total": {"$sum": "$qty"}}},
    {"$sort": {"total": -1}},
    {"$limit": 10},
]
for row in orders.aggregate(pipeline):
    print(row["_id"], row["total"])

Stage order is the whole game: put $match and $project before $group so the server filters before it builds groups. Pass allowDiskUse=True when a stage exceeds the 100MB in-memory limit.

Create indexes, including a unique onecreate-indexes

from pymongo import ASCENDING, DESCENDING

orders.create_index([("sku", ASCENDING)], unique=True)
orders.create_index([("status", ASCENDING), ("created_at", DESCENDING)])
orders.create_index("expires_at", expireAfterSeconds=0)   # TTL index

create_index is idempotent for an identical spec but raises if an index with the same name exists with different options. Creating a unique index on a collection that already has duplicates fails, so clean up first.

Catch the errors that actually happenhandle-write-errors

from pymongo.errors import DuplicateKeyError, OperationFailure

try:
    orders.insert_one({"_id": existing_id, "sku": "ABC"})
except DuplicateKeyError:
    orders.update_one({"_id": existing_id}, {"$set": {"sku": "ABC"}})
except OperationFailure as exc:
    print(exc.code, exc.details)

DuplicateKeyError is a subclass of WriteError, which is a subclass of OperationFailure, so order your except clauses narrowest first or the specific handler never runs.

Run multiple writes in a transactiontransactions

with client.start_session() as session:
    def transfer(session):
        accounts.update_one({"_id": 1}, {"$inc": {"balance": -100}}, session=session)
        accounts.update_one({"_id": 2}, {"$inc": {"balance": 100}}, session=session)

    session.with_transaction(transfer)

Pass session= to every operation inside the callback or it runs outside the transaction and silently commits on its own. with_transaction retries transient errors for you; start_transaction() does not. Needs a replica set or sharded cluster.

Use the asyncio APIasync-client

import asyncio
from pymongo import AsyncMongoClient

async def main():
    client = AsyncMongoClient("mongodb://localhost:27017")
    orders = client["shop"]["orders"]

    await orders.insert_one({"sku": "ABC", "qty": 3})
    async for doc in orders.find({"qty": {"$gte": 2}}):
        print(doc)
    await client.close()

asyncio.run(main())

Same method names as the sync client, but everything except find() must be awaited, and find() returns an async cursor you iterate with async for. This is the replacement for Motor, which was deprecated in May 2026.

Watch a collection for changeschange-streams

pipeline = [{"$match": {"operationType": {"$in": ["insert", "update"]}}}]

with orders.watch(pipeline, full_document="updateLookup") as stream:
    for change in stream:
        print(change["operationType"], change["fullDocument"])

Change streams need a replica set and read from the oplog, so a consumer that stops for longer than the oplog window cannot resume. Persist change["_id"] and pass it as resume_after to survive restarts.

Store a file bigger than 16MBgridfs-large-files

import gridfs

fs = gridfs.GridFS(db)
with open("report.pdf", "rb") as fh:
    file_id = fs.put(fh, filename="report.pdf", content_type="application/pdf")

data = fs.get(file_id).read()

GridFS chunks the file across two collections (fs.files and fs.chunks) to get around the 16MB BSON document limit. It is not a CDN; if you are serving these over HTTP, object storage is almost always the better answer.

Alternatives

PackageRegistryPick it when
beaniePyPIYou want async Pydantic models over MongoDB instead of raw dicts
mongoenginePyPIYou want a Django-style declarative ODM with field validation on a synchronous codebase
motorPyPIOnly for existing Tornado or asyncio code you cannot port yet; it was deprecated in May 2026 in favour of AsyncMongoClient