PyMongo's Native Async Driver: Moving Beyond Motor
Motor is deprecated. As of May 14, 2025, MongoDB officially deprecated Motor in favor of PyMongo 4.9's native async API. Motor will receive only bug fixes until it reaches end-of-life on May 14, 2026. After that, critical fixes only until May 14, 2027.
If you're building new Python applications that talk to MongoDB asynchronously, you need PyMongo's native async driver. If you're maintaining existing Motor codebases, you have two years to migrate. Here's what you need to know.
How PyMongo's Async Implementation Works
PyMongo 4.9 restructured its internals around a shared core that supports both sync and async operations. The pymongo.asynchronous module provides the async API. Both APIs share the same connection pooling, BSON encoding, and command construction logic. This eliminates the dual-maintenance burden and reduces overhead.
The async implementation uses Python's async/await syntax natively. Operations involving I/O (database commands, cursor iteration, collection operations) become awaitable coroutines. Configuration and client construction remain synchronous, which prevents startup blocking and aligns with asyncio best practices.
Getting Started: Basic Setup
Connection setup looks familiar if you've used the synchronous PyMongo driver:
from pymongo import AsyncMongoClient
client = AsyncMongoClient("mongodb://localhost:27017")
db = client["database_name"]
collection = db["collection_name"]
The AsyncMongoClient constructor is synchronous. Actual connection negotiation happens lazily on the first operation. This design prevents your application startup from blocking on database connectivity.
Core Operations
Document operations follow predictable async patterns. You await each operation:
# Insert
result = await collection.insert_one({"name": "Alice", "age": 30})
result = await collection.insert_many([
{"name": "Bob", "age": 25},
{"name": "Charlie", "age": 35}
])
# Query
user = await collection.find_one({"name": "Alice"})
count = await collection.count_documents({"age": {"$gte": 30}})
# Update
result = await collection.update_one(
{"name": "Alice"},
{"$set": {"age": 31}}
)
# Delete
result = await collection.delete_many({"age": {"$lt": 25}})
These operations return the same result objects as synchronous PyMongo, making migration straightforward.
The Aggregation Cursor Pattern
Aggregation handling differs from the synchronous API due to how async iteration works in Python. The aggregate() method returns a coroutine that resolves to an async cursor. You await the coroutine, then iterate over the cursor:
pipeline = [
{"$match": {"status": "active"}},
{"$group": {"_id": "$category", "total": {"$sum": "$amount"}}},
{"$sort": {"total": -1}}
]
cursor = await collection.aggregate(pipeline)
async for document in cursor:
print(f"{document['_id']}: {document['total']}")
For cases where you need all results materialized into a list, you build it manually:
async def aggregate_to_list(collection, pipeline):
results = []
cursor = await collection.aggregate(pipeline)
async for doc in cursor:
results.append(doc)
return results
This pattern applies to find() operations as well. The async cursor protocol gives you fine-grained control over memory consumption, which matters for large result sets.
Connection Management
Resource cleanup requires explicit client closure. The driver supports both manual cleanup and context managers:
# Manual cleanup
client = AsyncMongoClient("mongodb://localhost:27017")
try:
db = client["database"]
await db.command("ping")
finally:
client.close() # Note: close() is synchronous
# Context manager (preferred)
async with AsyncMongoClient("mongodb://localhost:27017") as client:
db = client["database"]
await db.command("ping")
The close() method is synchronous—connection teardown doesn't require awaiting. This aligns with Python's context manager protocol expectations.
Production Configuration
Production deployments need connection pooling and server API versioning configured:
from pymongo import AsyncMongoClient
from pymongo.server_api import ServerApi
client = AsyncMongoClient(
"mongodb+srv://cluster.mongodb.net/",
server_api=ServerApi("1"),
maxPoolSize=50,
minPoolSize=10,
maxIdleTimeMS=30000,
uuidRepresentation="standard"
)
The server_api parameter locks your application to a specific MongoDB server API version. This guarantees that server upgrades won't introduce breaking changes to your application.
Connection pool sizing balances concurrency capacity against resource consumption. maxPoolSize prevents connection exhaustion under load spikes. minPoolSize maintains warm connections for consistent latency. The maxIdleTimeMS setting closes connections that sit idle too long, freeing resources.
Complete CRUD Example
Here's a complete example showing all basic operations in context:
from pymongo import AsyncMongoClient
from pymongo.server_api import ServerApi
from datetime import datetime
async def main():
async with AsyncMongoClient(
"mongodb://localhost:27017",
server_api=ServerApi("1")
) as client:
db = client["app_database"]
users = db["users"]
# Create
user = {
"email": "user@example.com",
"created_at": datetime.utcnow(),
"status": "active"
}
result = await users.insert_one(user)
user_id = result.inserted_id
# Read
user = await users.find_one({"_id": user_id})
print(f"Created user: {user['email']}")
# Update
await users.update_one(
{"_id": user_id},
{"$set": {"last_login": datetime.utcnow()}}
)
# Aggregation
pipeline = [
{"$match": {"status": "active"}},
{"$group": {"_id": None, "count": {"$sum": 1}}}
]
cursor = await users.aggregate(pipeline)
async for doc in cursor:
print(f"Active users: {doc['count']}")
# Delete
await users.delete_one({"_id": user_id})
if __name__ == "__main__":
import asyncio
asyncio.run(main())
Migrating from Motor
If you're migrating from Motor, the changes are minimal. Import paths change from motor.motor_asyncio to pymongo. The biggest difference surfaces in aggregation cursor handling.
Motor provided a to_list() method on cursors:
# Motor
cursor = collection.aggregate(pipeline)
results = await cursor.to_list(length=None)
PyMongo requires manual iteration:
# PyMongo Async
cursor = await collection.aggregate(pipeline)
results = []
async for doc in cursor:
results.append(doc)
API method signatures remain compatible for most operations. Insert, find, update, and delete operations work the same way.
The migration eliminates an external dependency. Your application gains immediate access to new PyMongo features without waiting for Motor releases. Performance improves through the elimination of wrapper overhead. The magnitude depends on workload patterns—bulk operations see greater gains than single-document operations.
When to Make the Switch
For new projects, use PyMongo's async driver. The decision is straightforward—Motor is deprecated and PyMongo offers better performance and simpler maintenance.
For existing Motor codebases, you have until May 14, 2026 before end-of-life (critical fixes only until May 2027). This provides ample time to migrate. The migration is low-risk—API compatibility is high and the changes required are minimal.
The MongoDB Python ecosystem now has a single, well-maintained driver with both sync and async capabilities. This simplifies the development experience and reduces the maintenance burden. PyMongo 4.9's async support represents architectural maturation of the Python MongoDB driver ecosystem.