From bf4c24d85661f7eb0a8eeb6d09f5fe7ebcf5e9e6 Mon Sep 17 00:00:00 2001 From: Noah Stapp Date: Thu, 7 Aug 2025 10:01:24 -0400 Subject: [PATCH 1/4] PYTHON-3606 - Document best practice for closing MongoClients and cursors --- pymongo/asynchronous/collection.py | 7 +++++++ pymongo/asynchronous/mongo_client.py | 6 ++++++ pymongo/synchronous/collection.py | 7 +++++++ pymongo/synchronous/mongo_client.py | 6 ++++++ 4 files changed, 26 insertions(+) diff --git a/pymongo/asynchronous/collection.py b/pymongo/asynchronous/collection.py index 313c8c7c04..85318cba71 100644 --- a/pymongo/asynchronous/collection.py +++ b/pymongo/asynchronous/collection.py @@ -1776,6 +1776,13 @@ def find(self, *args: Any, **kwargs: Any) -> AsyncCursor[_DocumentType]: improper type. Returns an instance of :class:`~pymongo.asynchronous.cursor.AsyncCursor` corresponding to this query. + Best practice is to call :meth:`AsyncCursor.close` when the cursor is no longer needed, + or use the cursor in a with statement:: + + async with collection.find() as cursor: + async for doc in cursor: + print(doc) + The :meth:`find` method obeys the :attr:`read_preference` of this :class:`AsyncCollection`. diff --git a/pymongo/asynchronous/mongo_client.py b/pymongo/asynchronous/mongo_client.py index b988120d7c..b82e07ea48 100644 --- a/pymongo/asynchronous/mongo_client.py +++ b/pymongo/asynchronous/mongo_client.py @@ -202,6 +202,12 @@ def __init__( exception (recognizing that the operation failed) and then continue to execute. + Best practice is to call :meth:`AsyncMongoClient.close` when the client is no longer needed, + or use the client in a with statement:: + + with AsyncMongoClient(url) as client: + # Use client here. + The `host` parameter can be a full `mongodb URI `_, in addition to a simple hostname. It can also be a list of hostnames but no more diff --git a/pymongo/synchronous/collection.py b/pymongo/synchronous/collection.py index 32da83b0c2..d8209f38e9 100644 --- a/pymongo/synchronous/collection.py +++ b/pymongo/synchronous/collection.py @@ -1775,6 +1775,13 @@ def find(self, *args: Any, **kwargs: Any) -> Cursor[_DocumentType]: improper type. Returns an instance of :class:`~pymongo.cursor.Cursor` corresponding to this query. + Best practice is to call :meth:`Cursor.close` when the cursor is no longer needed, + or use the cursor in a with statement:: + + with collection.find() as cursor: + for doc in cursor: + print(doc) + The :meth:`find` method obeys the :attr:`read_preference` of this :class:`Collection`. diff --git a/pymongo/synchronous/mongo_client.py b/pymongo/synchronous/mongo_client.py index 5d95e9c9d5..6692aaed7d 100644 --- a/pymongo/synchronous/mongo_client.py +++ b/pymongo/synchronous/mongo_client.py @@ -199,6 +199,12 @@ def __init__( exception (recognizing that the operation failed) and then continue to execute. + Best practice is to call :meth:`MongoClient.close` when the client is no longer needed, + or use the client in a with statement:: + + with MongoClient(url) as client: + # Use client here. + The `host` parameter can be a full `mongodb URI `_, in addition to a simple hostname. It can also be a list of hostnames but no more From 5f1ee383d475edb233d8d090a2fda71b46a99061 Mon Sep 17 00:00:00 2001 From: Noah Stapp Date: Thu, 7 Aug 2025 11:49:17 -0400 Subject: [PATCH 2/4] Call close before raising (Async)StopIteration --- pymongo/asynchronous/cursor.py | 4 ++++ pymongo/synchronous/cursor.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/pymongo/asynchronous/cursor.py b/pymongo/asynchronous/cursor.py index ab2d0e873c..ef773c38d9 100644 --- a/pymongo/asynchronous/cursor.py +++ b/pymongo/asynchronous/cursor.py @@ -1263,10 +1263,14 @@ async def next(self) -> _DocumentType: self._exhaust_checked = True await self._supports_exhaust() if self._empty: + if self._cursor_type == CursorType.NON_TAILABLE: + await self.close() raise StopAsyncIteration if len(self._data) or await self._refresh(): return self._data.popleft() else: + if self._cursor_type == CursorType.NON_TAILABLE: + await self.close() raise StopAsyncIteration async def _next_batch(self, result: list, total: Optional[int] = None) -> bool: # type: ignore[type-arg] diff --git a/pymongo/synchronous/cursor.py b/pymongo/synchronous/cursor.py index eb45d9c5d1..1d8b43b428 100644 --- a/pymongo/synchronous/cursor.py +++ b/pymongo/synchronous/cursor.py @@ -1261,10 +1261,14 @@ def next(self) -> _DocumentType: self._exhaust_checked = True self._supports_exhaust() if self._empty: + if self._cursor_type == CursorType.NON_TAILABLE: + self.close() raise StopIteration if len(self._data) or self._refresh(): return self._data.popleft() else: + if self._cursor_type == CursorType.NON_TAILABLE: + self.close() raise StopIteration def _next_batch(self, result: list, total: Optional[int] = None) -> bool: # type: ignore[type-arg] From cc68c7c56e69c0268ab3139fa585aa1afb37bfe3 Mon Sep 17 00:00:00 2001 From: Noah Stapp Date: Fri, 8 Aug 2025 12:12:59 -0400 Subject: [PATCH 3/4] Doc-only changes --- pymongo/asynchronous/collection.py | 4 +++- pymongo/asynchronous/cursor.py | 4 ---- pymongo/asynchronous/mongo_client.py | 2 +- pymongo/synchronous/collection.py | 4 +++- pymongo/synchronous/cursor.py | 4 ---- 5 files changed, 7 insertions(+), 11 deletions(-) diff --git a/pymongo/asynchronous/collection.py b/pymongo/asynchronous/collection.py index 85318cba71..6970bd5b90 100644 --- a/pymongo/asynchronous/collection.py +++ b/pymongo/asynchronous/collection.py @@ -1776,7 +1776,9 @@ def find(self, *args: Any, **kwargs: Any) -> AsyncCursor[_DocumentType]: improper type. Returns an instance of :class:`~pymongo.asynchronous.cursor.AsyncCursor` corresponding to this query. - Best practice is to call :meth:`AsyncCursor.close` when the cursor is no longer needed, + Cursors are closed automatically when they are exhausted (the last batch of data is retrieved from the database). + If a cursor is not exhausted, it will be closed automatically upon garbage collection, which leaves resources open but unused for a potentially long period of time. + To avoid this, best practice is to call :meth:`AsyncCursor.close` when the cursor is no longer needed, or use the cursor in a with statement:: async with collection.find() as cursor: diff --git a/pymongo/asynchronous/cursor.py b/pymongo/asynchronous/cursor.py index ef773c38d9..ab2d0e873c 100644 --- a/pymongo/asynchronous/cursor.py +++ b/pymongo/asynchronous/cursor.py @@ -1263,14 +1263,10 @@ async def next(self) -> _DocumentType: self._exhaust_checked = True await self._supports_exhaust() if self._empty: - if self._cursor_type == CursorType.NON_TAILABLE: - await self.close() raise StopAsyncIteration if len(self._data) or await self._refresh(): return self._data.popleft() else: - if self._cursor_type == CursorType.NON_TAILABLE: - await self.close() raise StopAsyncIteration async def _next_batch(self, result: list, total: Optional[int] = None) -> bool: # type: ignore[type-arg] diff --git a/pymongo/asynchronous/mongo_client.py b/pymongo/asynchronous/mongo_client.py index b82e07ea48..5b7de1b299 100644 --- a/pymongo/asynchronous/mongo_client.py +++ b/pymongo/asynchronous/mongo_client.py @@ -205,7 +205,7 @@ def __init__( Best practice is to call :meth:`AsyncMongoClient.close` when the client is no longer needed, or use the client in a with statement:: - with AsyncMongoClient(url) as client: + async with AsyncMongoClient(url) as client: # Use client here. The `host` parameter can be a full `mongodb URI diff --git a/pymongo/synchronous/collection.py b/pymongo/synchronous/collection.py index d8209f38e9..07217c2f0e 100644 --- a/pymongo/synchronous/collection.py +++ b/pymongo/synchronous/collection.py @@ -1775,7 +1775,9 @@ def find(self, *args: Any, **kwargs: Any) -> Cursor[_DocumentType]: improper type. Returns an instance of :class:`~pymongo.cursor.Cursor` corresponding to this query. - Best practice is to call :meth:`Cursor.close` when the cursor is no longer needed, + Cursors are closed automatically when they are exhausted (the last batch of data is retrieved from the database). + If a cursor is not exhausted, it will be closed automatically upon garbage collection, which leaves resources open but unused for a potentially long period of time. + To avoid this, best practice is to call :meth:`Cursor.close` when the cursor is no longer needed, or use the cursor in a with statement:: with collection.find() as cursor: diff --git a/pymongo/synchronous/cursor.py b/pymongo/synchronous/cursor.py index 1d8b43b428..eb45d9c5d1 100644 --- a/pymongo/synchronous/cursor.py +++ b/pymongo/synchronous/cursor.py @@ -1261,14 +1261,10 @@ def next(self) -> _DocumentType: self._exhaust_checked = True self._supports_exhaust() if self._empty: - if self._cursor_type == CursorType.NON_TAILABLE: - self.close() raise StopIteration if len(self._data) or self._refresh(): return self._data.popleft() else: - if self._cursor_type == CursorType.NON_TAILABLE: - self.close() raise StopIteration def _next_batch(self, result: list, total: Optional[int] = None) -> bool: # type: ignore[type-arg] From 1bc02738fb5f3825e07e37d7c2fd4113103dec74 Mon Sep 17 00:00:00 2001 From: Noah Stapp Date: Mon, 11 Aug 2025 12:04:43 -0400 Subject: [PATCH 4/4] Address review --- pymongo/asynchronous/collection.py | 27 +++++++++++++++++++++++++++ pymongo/asynchronous/database.py | 18 ++++++++++++++++-- pymongo/asynchronous/mongo_client.py | 9 +++++++++ pymongo/synchronous/collection.py | 27 +++++++++++++++++++++++++++ pymongo/synchronous/database.py | 14 ++++++++++++++ pymongo/synchronous/mongo_client.py | 9 +++++++++ 6 files changed, 102 insertions(+), 2 deletions(-) diff --git a/pymongo/asynchronous/collection.py b/pymongo/asynchronous/collection.py index 6970bd5b90..ac207c75d5 100644 --- a/pymongo/asynchronous/collection.py +++ b/pymongo/asynchronous/collection.py @@ -2512,6 +2512,15 @@ async def list_indexes( ... SON([('v', 2), ('key', SON([('_id', 1)])), ('name', '_id_')]) + Cursors are closed automatically when they are exhausted (the last batch of data is retrieved from the database). + If a cursor is not exhausted, it will be closed automatically upon garbage collection, which leaves resources open but unused for a potentially long period of time. + To avoid this, best practice is to call :meth:`AsyncCursor.close` when the cursor is no longer needed, + or use the cursor in a with statement:: + + async with await collection.list_indexes() as cursor: + async for index in cursor: + print(index) + :param session: a :class:`~pymongo.asynchronous.client_session.AsyncClientSession`. :param comment: A user-provided comment to attach to this @@ -2629,6 +2638,15 @@ async def list_search_indexes( ) -> AsyncCommandCursor[Mapping[str, Any]]: """Return a cursor over search indexes for the current collection. + Cursors are closed automatically when they are exhausted (the last batch of data is retrieved from the database). + If a cursor is not exhausted, it will be closed automatically upon garbage collection, which leaves resources open but unused for a potentially long period of time. + To avoid this, best practice is to call :meth:`AsyncCursor.close` when the cursor is no longer needed, + or use the cursor in a with statement:: + + async with await collection.list_search_indexes() as cursor: + async for index in cursor: + print(index) + :param name: If given, the name of the index to search for. Only indexes with matching index names will be returned. If not given, all search indexes for the current collection @@ -2931,6 +2949,15 @@ async def aggregate( .. note:: The :attr:`~pymongo.asynchronous.collection.AsyncCollection.write_concern` of this collection is automatically applied to this operation. + Cursors are closed automatically when they are exhausted (the last batch of data is retrieved from the database). + If a cursor is not exhausted, it will be closed automatically upon garbage collection, which leaves resources open but unused for a potentially long period of time. + To avoid this, best practice is to call :meth:`AsyncCursor.close` when the cursor is no longer needed, + or use the cursor in a with statement:: + + async with await collection.aggregate() as cursor: + async for operation in cursor: + print(operation) + :param pipeline: a list of aggregation pipeline stages :param session: a :class:`~pymongo.asynchronous.client_session.AsyncClientSession`. diff --git a/pymongo/asynchronous/database.py b/pymongo/asynchronous/database.py index 09713c37ec..381f481d8a 100644 --- a/pymongo/asynchronous/database.py +++ b/pymongo/asynchronous/database.py @@ -643,8 +643,8 @@ async def aggregate( .. code-block:: python # Lists all operations currently running on the server. - with client.admin.aggregate([{"$currentOp": {}}]) as cursor: - for operation in cursor: + async with await client.admin.aggregate([{"$currentOp": {}}]) as cursor: + async for operation in cursor: print(operation) The :meth:`aggregate` method obeys the :attr:`read_preference` of this @@ -652,6 +652,11 @@ async def aggregate( which case :attr:`~pymongo.read_preferences.ReadPreference.PRIMARY` is used. + Cursors are closed automatically when they are exhausted (the last batch of data is retrieved from the database). + If a cursor is not exhausted, it will be closed automatically upon garbage collection, which leaves resources open but unused for a potentially long period of time. + To avoid this, best practice is to call :meth:`AsyncCursor.close` when the cursor is no longer needed, + or use the cursor in a with statement. + .. note:: This method does not support the 'explain' option. Please use :meth:`~pymongo.asynchronous.database.AsyncDatabase.command` instead. @@ -1154,6 +1159,15 @@ async def list_collections( ) -> AsyncCommandCursor[MutableMapping[str, Any]]: """Get a cursor over the collections of this database. + Cursors are closed automatically when they are exhausted (the last batch of data is retrieved from the database). + If a cursor is not exhausted, it will be closed automatically upon garbage collection, which leaves resources open but unused for a potentially long period of time. + To avoid this, best practice is to call :meth:`AsyncCursor.close` when the cursor is no longer needed, + or use the cursor in a with statement:: + + async with await database.list_collections() as cursor: + async for collection in cursor: + print(collection) + :param session: a :class:`~pymongo.asynchronous.client_session.AsyncClientSession`. :param filter: A query document to filter the list of diff --git a/pymongo/asynchronous/mongo_client.py b/pymongo/asynchronous/mongo_client.py index 5b7de1b299..c8ef9c1dca 100644 --- a/pymongo/asynchronous/mongo_client.py +++ b/pymongo/asynchronous/mongo_client.py @@ -2351,6 +2351,15 @@ async def list_databases( ) -> AsyncCommandCursor[dict[str, Any]]: """Get a cursor over the databases of the connected server. + Cursors are closed automatically when they are exhausted (the last batch of data is retrieved from the database). + If a cursor is not exhausted, it will be closed automatically upon garbage collection, which leaves resources open but unused for a potentially long period of time. + To avoid this, best practice is to call :meth:`AsyncCursor.close` when the cursor is no longer needed, + or use the cursor in a with statement:: + + async with await client.list_databases() as cursor: + async for database in cursor: + print(database) + :param session: a :class:`~pymongo.asynchronous.client_session.AsyncClientSession`. :param comment: A user-provided comment to attach to this diff --git a/pymongo/synchronous/collection.py b/pymongo/synchronous/collection.py index 07217c2f0e..5ee31112ed 100644 --- a/pymongo/synchronous/collection.py +++ b/pymongo/synchronous/collection.py @@ -2509,6 +2509,15 @@ def list_indexes( ... SON([('v', 2), ('key', SON([('_id', 1)])), ('name', '_id_')]) + Cursors are closed automatically when they are exhausted (the last batch of data is retrieved from the database). + If a cursor is not exhausted, it will be closed automatically upon garbage collection, which leaves resources open but unused for a potentially long period of time. + To avoid this, best practice is to call :meth:`Cursor.close` when the cursor is no longer needed, + or use the cursor in a with statement:: + + with collection.list_indexes() as cursor: + for index in cursor: + print(index) + :param session: a :class:`~pymongo.client_session.ClientSession`. :param comment: A user-provided comment to attach to this @@ -2626,6 +2635,15 @@ def list_search_indexes( ) -> CommandCursor[Mapping[str, Any]]: """Return a cursor over search indexes for the current collection. + Cursors are closed automatically when they are exhausted (the last batch of data is retrieved from the database). + If a cursor is not exhausted, it will be closed automatically upon garbage collection, which leaves resources open but unused for a potentially long period of time. + To avoid this, best practice is to call :meth:`Cursor.close` when the cursor is no longer needed, + or use the cursor in a with statement:: + + with collection.list_search_indexes() as cursor: + for index in cursor: + print(index) + :param name: If given, the name of the index to search for. Only indexes with matching index names will be returned. If not given, all search indexes for the current collection @@ -2924,6 +2942,15 @@ def aggregate( .. note:: The :attr:`~pymongo.collection.Collection.write_concern` of this collection is automatically applied to this operation. + Cursors are closed automatically when they are exhausted (the last batch of data is retrieved from the database). + If a cursor is not exhausted, it will be closed automatically upon garbage collection, which leaves resources open but unused for a potentially long period of time. + To avoid this, best practice is to call :meth:`Cursor.close` when the cursor is no longer needed, + or use the cursor in a with statement:: + + with collection.aggregate() as cursor: + for operation in cursor: + print(operation) + :param pipeline: a list of aggregation pipeline stages :param session: a :class:`~pymongo.client_session.ClientSession`. diff --git a/pymongo/synchronous/database.py b/pymongo/synchronous/database.py index dd9ea01558..c0d9b2ee5a 100644 --- a/pymongo/synchronous/database.py +++ b/pymongo/synchronous/database.py @@ -652,6 +652,11 @@ def aggregate( which case :attr:`~pymongo.read_preferences.ReadPreference.PRIMARY` is used. + Cursors are closed automatically when they are exhausted (the last batch of data is retrieved from the database). + If a cursor is not exhausted, it will be closed automatically upon garbage collection, which leaves resources open but unused for a potentially long period of time. + To avoid this, best practice is to call :meth:`Cursor.close` when the cursor is no longer needed, + or use the cursor in a with statement. + .. note:: This method does not support the 'explain' option. Please use :meth:`~pymongo.database.Database.command` instead. @@ -1148,6 +1153,15 @@ def list_collections( ) -> CommandCursor[MutableMapping[str, Any]]: """Get a cursor over the collections of this database. + Cursors are closed automatically when they are exhausted (the last batch of data is retrieved from the database). + If a cursor is not exhausted, it will be closed automatically upon garbage collection, which leaves resources open but unused for a potentially long period of time. + To avoid this, best practice is to call :meth:`Cursor.close` when the cursor is no longer needed, + or use the cursor in a with statement:: + + with database.list_collections() as cursor: + for collection in cursor: + print(collection) + :param session: a :class:`~pymongo.client_session.ClientSession`. :param filter: A query document to filter the list of diff --git a/pymongo/synchronous/mongo_client.py b/pymongo/synchronous/mongo_client.py index 6692aaed7d..19a72c2180 100644 --- a/pymongo/synchronous/mongo_client.py +++ b/pymongo/synchronous/mongo_client.py @@ -2341,6 +2341,15 @@ def list_databases( ) -> CommandCursor[dict[str, Any]]: """Get a cursor over the databases of the connected server. + Cursors are closed automatically when they are exhausted (the last batch of data is retrieved from the database). + If a cursor is not exhausted, it will be closed automatically upon garbage collection, which leaves resources open but unused for a potentially long period of time. + To avoid this, best practice is to call :meth:`Cursor.close` when the cursor is no longer needed, + or use the cursor in a with statement:: + + with client.list_databases() as cursor: + for database in cursor: + print(database) + :param session: a :class:`~pymongo.client_session.ClientSession`. :param comment: A user-provided comment to attach to this