Documentation: context manager `__exit__` exception behavior wording is incomplete
The documentation for [context manager protocol](https://docs.python.org/3/library/stdtypes.html#contextmanager.exit) currently says:
"Exceptions that occur during execution of this method will replace any exception that occurred in the body of the with statement."
This phrasing suggests that the original exception is discarded. In practice (since Python 3.0, when implicit exception chaining was introduced), an exception raised inside __exit__ becomes the one that propagates, but the original exception is not lost: it is attached as the new exception’s __context__.
The effect is that tracebacks show:
Traceback (most recent call last):
...
During handling of the above exception, another exception occurred:
...
So the original error remains visible to users and available in the exception object, rather than being fully replaced.
Suggested improvement
Change the sentence to something like:
"Exceptions that occur during execution of this method replace the exception from the body of the
withstatement, with the original preserved as context."
Linked PRs
- gh-140169
- gh-142792
- gh-142793
Exceptions that occur during execution of this method will replace any exception that occurred in the body of the with statement
This is actually correct though. The exception is replaced:
>>> class A:
... def __enter__(self): return self
... def __exit__(self, exc_type, *a):
... print(exc_type)
... raise ValueError("oh no")
...
>>> try:
... with A():
... 1/0
... except ZeroDivisionError:
... assert False, "nope"
...
<class 'ZeroDivisionError'>
Traceback (most recent call last):
File "<python-input-42>", line 3, in <module>
raise ZeroDivisionError() from None
ZeroDivisionError
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "<python-input-42>", line 2, in <module>
with A():
~^^
File "<python-input-36>", line 5, in __exit__
raise ValueError("oh no")
ValueError: oh no
In the above example, the assertion error will never be raised so the exception is indeed replaced (it doesn't say anything about being lost). The fact that the old exception is now accessible in __context__ is already documented in https://docs.python.org/3/library/exceptions.html#BaseException.context (it mentions with, but this is not the only possibility; there are other cases when an exception replaces another and updates its __context__).
I'm not really against adding this small note about the original exception being in the __context__ but I think this means we're documenting twice something at two different places (and I don't think we should document this in __exit__; I think this should remain documented as part of BaseException as this affects any code that "replace" exceptions).
This is actually correct though
is already documented
I fully agree; I'm reporting from my own perspective of (initial, though not permanent) confusion about the behavior.
but I think this means we're documenting twice something at two different places
It indeed does -- whether that is a good thing or a bad thing is always dependent on the "policy" of the Python documentation, i.e. on how to ultimately evaluate the trade-off between:
- loss of clarity because of additional verbosity
- loss of clarity because of excessive cross-referencing
I agree that the documentation is correct but not fully precise.
Perhaps we can just rephrase it as:
Exceptions that occur during execution of this method will
-replace any exception that occurred in the body of the
+be propagated on top of any exception that occurred in the body of the
with statement.
I'll open a PR.