Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions doc/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ PyMongo 4.16 brings a number of changes including:
- Removed support for Eventlet.
Eventlet is actively being sunset by its maintainers and has compatibility issues with PyMongo's dnspython dependency.
- Use Zstandard support from the standard library for Python 3.14+, and use ``backports.zstd`` for older versions.
- Fixed return type annotation for ``find_one_and_*`` methods on :class:`~pymongo.asynchronous.collection.AsyncCollection`
and :class:`~pymongo.synchronous.collection.Collection` to include ``None``.

Changes in Version 4.15.4 (2025/10/21)
--------------------------------------
Expand Down
1 change: 1 addition & 0 deletions doc/contributors.rst
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,4 @@ The following is a list of people who have contributed to
- Jeffrey A. Clark (aclark4life)
- Steven Silvester (blink1073)
- Noah Stapp (NoahStapp)
- Cal Jacobson (cj81499)
28 changes: 24 additions & 4 deletions pymongo/asynchronous/collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -3310,7 +3310,7 @@ async def find_one_and_delete(
let: Optional[Mapping[str, Any]] = None,
comment: Optional[Any] = None,
**kwargs: Any,
) -> _DocumentType:
) -> Optional[_DocumentType]:
"""Finds a single document and deletes it, returning the document.

>>> await db.test.count_documents({'x': 1})
Expand All @@ -3320,6 +3320,10 @@ async def find_one_and_delete(
>>> await db.test.count_documents({'x': 1})
1

Returns ``None`` if no document matches the filter.

>>> await db.test.find_one_and_delete({'_exists': False})

If multiple documents match *filter*, a *sort* can be applied.

>>> async for doc in db.test.find({'x': 1}):
Expand Down Expand Up @@ -3402,10 +3406,22 @@ async def find_one_and_replace(
let: Optional[Mapping[str, Any]] = None,
comment: Optional[Any] = None,
**kwargs: Any,
) -> _DocumentType:
) -> Optional[_DocumentType]:
"""Finds a single document and replaces it, returning either the
original or the replaced document.

>>> await db.test.find_one({'x': 1})
{'_id': 0, 'x': 1}
>>> await db.test.find_one_and_replace({'x': 1}, {'y': 2})
{'_id': 0, 'x': 1}
>>> await db.test.find_one({'x': 1})
>>> await db.test.find_one({'y': 2})
{'_id': 0, 'y': 2}

Returns ``None`` if no document matches the filter.

>>> await db.test.find_one_and_replace({'_exists': False}, {'x': 1})

The :meth:`find_one_and_replace` method differs from
:meth:`find_one_and_update` by replacing the document matched by
*filter*, rather than modifying the existing document.
Expand Down Expand Up @@ -3510,13 +3526,17 @@ async def find_one_and_update(
let: Optional[Mapping[str, Any]] = None,
comment: Optional[Any] = None,
**kwargs: Any,
) -> _DocumentType:
) -> Optional[_DocumentType]:
Copy link

@ZachOldham ZachOldham Nov 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As someone not familiar with the ins-and-outs of this repo, is Optional[<type>] preferred over <type> | None? Or is it that it needs to support python versions that don't have the | syntax? (Same comment goes for the other instance of this as well)

Copy link
Author

@cj81499 cj81499 Nov 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For reviewers, Zach is my coworker looking over this PR before I mark it as ready for review.

pymongo supports Python 3.9+. See requires-python = ">=3.9" in pyproject.toml.

Support for the "|" syntax for a Union was added with Python 3.10 (PEP 604).

Regardless of what is/isn't supported/required, using Optional (rather than |) is more consistent with the rest of the code.

"""Finds a single document and updates it, returning either the
original or the updated document.

>>> await db.test.find_one({'_id': 665})
{'_id': 665, 'done': False, 'count': 25}
>>> await db.test.find_one_and_update(
... {'_id': 665}, {'$inc': {'count': 1}, '$set': {'done': True}})
{'_id': 665, 'done': False, 'count': 25}}
{'_id': 665, 'done': False, 'count': 25}
>>> await db.test.find_one({'_id': 665})
{'_id': 665, 'done': True, 'count': 26}

Returns ``None`` if no document matches the filter.

Expand Down
28 changes: 24 additions & 4 deletions pymongo/synchronous/collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -3303,7 +3303,7 @@ def find_one_and_delete(
let: Optional[Mapping[str, Any]] = None,
comment: Optional[Any] = None,
**kwargs: Any,
) -> _DocumentType:
) -> Optional[_DocumentType]:
"""Finds a single document and deletes it, returning the document.

>>> db.test.count_documents({'x': 1})
Expand All @@ -3313,6 +3313,10 @@ def find_one_and_delete(
>>> db.test.count_documents({'x': 1})
1

Returns ``None`` if no document matches the filter.

>>> db.test.find_one_and_delete({'_exists': False})

If multiple documents match *filter*, a *sort* can be applied.

>>> for doc in db.test.find({'x': 1}):
Expand Down Expand Up @@ -3395,10 +3399,22 @@ def find_one_and_replace(
let: Optional[Mapping[str, Any]] = None,
comment: Optional[Any] = None,
**kwargs: Any,
) -> _DocumentType:
) -> Optional[_DocumentType]:
"""Finds a single document and replaces it, returning either the
original or the replaced document.

>>> db.test.find_one({'x': 1})
{'_id': 0, 'x': 1}
>>> db.test.find_one_and_replace({'x': 1}, {'y': 2})
{'_id': 0, 'x': 1}
>>> db.test.find_one({'x': 1})
>>> db.test.find_one({'y': 2})
{'_id': 0, 'y': 2}

Returns ``None`` if no document matches the filter.

>>> db.test.find_one_and_replace({'_exists': False}, {'x': 1})

The :meth:`find_one_and_replace` method differs from
:meth:`find_one_and_update` by replacing the document matched by
*filter*, rather than modifying the existing document.
Expand Down Expand Up @@ -3503,13 +3519,17 @@ def find_one_and_update(
let: Optional[Mapping[str, Any]] = None,
comment: Optional[Any] = None,
**kwargs: Any,
) -> _DocumentType:
) -> Optional[_DocumentType]:
"""Finds a single document and updates it, returning either the
original or the updated document.

>>> db.test.find_one({'_id': 665})
{'_id': 665, 'done': False, 'count': 25}
>>> db.test.find_one_and_update(
... {'_id': 665}, {'$inc': {'count': 1}, '$set': {'done': True}})
{'_id': 665, 'done': False, 'count': 25}}
{'_id': 665, 'done': False, 'count': 25}
>>> db.test.find_one({'_id': 665})
{'_id': 665, 'done': True, 'count': 26}

Returns ``None`` if no document matches the filter.

Expand Down