pymongo review
PyMongo 4.17.0 is MongoDB's official Python driver and includes synchronous and asyncio clients, the BSON implementation MongoDB expects, and GridFS. It sends Python dictionaries through collection methods such as find, aggregate, insert_one, bulk_write, and watch without imposing an object model. Version 4.17 improves session-bound operations, BSON encode and decode performance, and server selection under overload, while deprecating old Python 2-style SON methods before 5.0.
PyMongo 4.17.0 installed in 0.4 seconds and used 8 MB in our sandbox, with 0 known vulnerabilities and a successful 0.14-second bson import. Use it for direct MongoDB access, but add database validation or an ODM when plain dictionaries make silent field mistakes too easy.
We installed it
| Install | ✓ · 0.4s | 2 packages on disk · 8 MB |
| Import | ✓ | import bson in 0.14s · compiled extensions · py.typed · requires Python >=3.9 |
| Known vulns | 0 | (pip-audit) |
Answers from our run
Does pymongo install cleanly?
Yes. In a fresh container with an empty cache, pip install pymongo finished in 0.4s, leaving 2 packages and 8 MB on disk. pip-audit reported no known vulnerabilities.
What does pymongo need to run?
Python >=3.9, and a platform wheel with compiled extensions. In our run import bson succeeded in 0.14s, and the package ships py.typed for type checkers.
pymongo or beanie: which should you use?
beanie: Use it when an asyncio service wants Pydantic document models and ODM query helpers. PyMongo 4.17.0 installed in 0.4 seconds and used 8 MB in our sandbox, with 0 known vulnerabilities and a successful 0.14-second bson import.
When should you not use pymongo?
You expect model validation at the driver boundary; a misspelled filter key quietly matches nothing and a misspelled update field can become stored data
Use it if
- Python code needs the vendor-maintained driver for MongoDB or Atlas
- Plain dictionaries and MongoDB operators fit better than ODM model classes
- One project needs both synchronous jobs and native asyncio access
- The application uses sessions, change streams, GridFS, field-level encryption, or MongoDB-specific authentication
- You expect model validation at the driver boundary; a misspelled filter key quietly matches nothing and a misspelled update field can become stored data
- The domain relies on relational joins and foreign keys; lookup pipelines cannot make a document database enforce a relational model
- Local development uses a standalone mongod but the application requires multi-document transactions; transactions need a replica set or sharded cluster
- A prefork server constructs its client before workers start; MongoClient owns pools and monitoring threads and must be created after the fork
- The code follows PyMongo 3 examples that call save, insert, update, remove, count, or cursor.count; PyMongo 4 removed those legacy shortcuts
Setup reality
We installed PyMongo 4.17.0 in 0.4 seconds on Python 3.12. Two packages occupied 8 MB, pip-audit reported 0 known vulnerabilities, and import bson completed in 0.14 seconds. The wheel contains compiled .so extensions and py.typed metadata, requires Python 3.9 or newer, and declares 23 direct dependencies. Its installed metadata did not identify a license.
Never install the separate bson package from PyPI. PyMongo already ships its required bson module, and the unrelated distribution can shadow it. Standard mongodb:// connections need no file-based config. mongodb+srv:// discovery needs dnspython. Store credentials and TLS options outside source, and percent-encode reserved characters in URI usernames or passwords. Extras add AWS, GSSAPI, OCSP, compression, and client-side encryption dependencies.
Constructing MongoClient does not prove the address is reachable. It starts topology monitoring and waits for an operation that needs a server. Use client.admin.command('ping') in readiness checks and choose serverSelectionTimeoutMS so a bad URI does not stall the first request for an unexpected interval. Create 1 client per process after any fork and reuse its connection pool.
Cursors fetch subsequent batches while you iterate. A session cannot be shared across concurrent threads or asyncio tasks, and transactions require a replica set or sharded cluster. Version 4.17 adds session-binding context managers, but the scope still has to cover every intended operation. AsyncMongoClient collection methods are awaited, while find returns an async cursor. Type hints do not replace MongoDB schema validation or unique indexes.
Patterns
Reuse one client and insert a document connect-insert
from pymongo import MongoClient
client = MongoClient('mongodb://localhost:27017')
orders = client['shop']['orders']
result = orders.insert_one({'sku': 'ABC', 'qty': 3})
print(result.inserted_id)Client construction starts background monitoring but does not confirm a usable server. Create it after any process fork.
Fail startup on an unreachable server ping-server
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 MongoDB: {exc}')Without an explicit operation, a bad URI can stay hidden until the first request needs server selection.
Filter, project, sort, and limit query-project
from pymongo import DESCENDING
cursor = (
orders.find({'qty': {'$gte': 2}}, {'sku': 1, 'qty': 1, '_id': 0})
.sort('qty', DESCENDING)
.limit(20)
)
for document in cursor:
print(document)find returns a lazy cursor. Iteration can trigger additional network batches, and a consumed cursor should not be reused for a second pass.
Update or create one document update-upsert
result = orders.update_one(
{'sku': 'ABC'},
{'$set': {'qty': 10}, '$currentDate': {'updated_at': True}},
upsert=True,
)
print(result.matched_count, result.modified_count, result.upserted_id)Update documents require operators such as $set. Use replace_one when you mean to replace every field except _id.
Send mixed writes as a batch bulk-operations
from pymongo import InsertOne, UpdateOne, DeleteOne
operations = [
InsertOne({'sku': 'NEW', 'qty': 1}),
UpdateOne({'sku': 'ABC'}, {'$inc': {'qty': 1}}),
DeleteOne({'sku': 'OLD'}),
]
result = orders.bulk_write(operations, ordered=False)Unordered mode continues after an individual failure and may reorder independent operations. Inspect BulkWriteError details before claiming the batch succeeded.
Group and rank server-side aggregate
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'])Put selective match and project stages early. A large blocking stage may require allowDiskUse=True and a supporting index.
Enforce uniqueness and add a TTL create-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)A new unique index fails when existing documents contain duplicates. TTL deletion is asynchronous and should not be used as an exact scheduler.
Catch a duplicate key separately handle-duplicate
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 inherits from broader operation exceptions. Catch the specific error first when it has its own recovery path.
Keep related writes in one session run-transaction
with client.start_session() as session:
def transfer(active_session):
accounts.update_one(
{'_id': 1}, {'$inc': {'balance': -100}}, session=active_session,
)
accounts.update_one(
{'_id': 2}, {'$inc': {'balance': 100}}, session=active_session,
)
session.with_transaction(transfer)Transactions require a replica set or sharded cluster. The helper may retry qualifying failures, so the callback must tolerate another execution.
Query with the built-in asyncio client use-async-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 document in orders.find({'qty': {'$gte': 2}}):
print(document)
await client.close()
asyncio.run(main())Collection operations are awaited, while find returns an async cursor for async for. Do not share one session across concurrent tasks.
Resume a collection change stream watch-changes
pipeline = [{'$match': {'operationType': {'$in': ['insert', 'update']}}}]
with orders.watch(pipeline, full_document='updateLookup') as stream:
for change in stream:
save_resume_token(change['_id'])
print(change['operationType'], change['fullDocument'])Change streams rely on the replica-set oplog. Persist the resume token, but an outage beyond oplog history can still make it unusable.
Put a large file in GridFS store-gridfs
import gridfs
fs = gridfs.GridFS(db)
with open('report.pdf', 'rb') as handle:
file_id = fs.put(handle, filename='report.pdf', content_type='application/pdf')
with fs.get(file_id) as stored:
data = stored.read()GridFS splits bytes across files and chunks collections to bypass the BSON document limit. Object storage is usually a better HTTP download origin.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| beanie | PyPI | Use it when an asyncio service wants Pydantic document models and ODM query helpers |
| mongoengine | PyPI | Use it for synchronous document classes with declared fields, validation, and references |
| motor | PyPI | Keep it while migrating existing Motor code; start new asyncio work with PyMongo's AsyncMongoClient |
More data guides
numpy · fsspec · pandas · sqlalchemy · pyarrow · lxml · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

