You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Behavioral differences between the with-statement and ExitStack.enter_context #144386
I noticed a behavioral difference between the (async) with statement and (Async)ExitStack. The with statement correctly recognizes context manager methods (__enter__, __exit__, __aenter__, and __aexit__) even when they are defined via __slots__, whereas (Async)ExitStack does not.
classSlotsCM:
__slots__= ("__enter__", "__exit__", )
def__init__(self):
self.__enter__=self.__exit__=lambda*__: None# This workswithSlotsCM():
pass# This doesn't.fromcontextllibimportExitStackwithExitStack() asstack:
stack.enter_context(SlotsCM())
Traceback (most recent call last):
File "xxxx.py", line 16, in <module>
stack.enter_context(SlotsCM())
~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^
File "......../contextlib.py", line 530, in enter_context
result = _enter(cm)
TypeError: 'member_descriptor' object is not callable
contextlib.ExitStack.enter_context(cm) calls type(cm).__enter__(cm). The problem looks like that type(cm) does not recognize those __enter__ and __exit__ methods defined by __slots__. The document says type() generally returns the same object as returned by object.__class__, which throws the same TypeError:
classSlotsCM:
__slots__= (
"__enter__",
"__exit__",
)
def__init__(self):
self.__enter__=self.__exit__=lambda*__: Nones=SlotsCM()
s.__enter__(s) # This works.type(s).__enter__(s) # TypeError: 'member_descriptor' object is not callables.__class__.__enter__(s) # TypeError: 'member_descriptor' object is not callable
I don't fully understand the error message, but yes, the cause of the issue seems to be that (Async)ExitStack.enter_(async)_context() looks up dunder methods on the class rather than on
the instance.
I am not familiar with the member_descriptor neither, it looks like a concept in C. A printing shows it refers to the __enter__ object reserved by __slots__.
Also I found that calling __get__(s) on the member descriptor returns the same function object as s.__enter__.
I just found that there is also a difference between the actual with-statement behavior and its documented semantically equivalent code. The documentted equivalent code throws the same TypeError as contextlib.ExitStack.enter_context().
changed the title [-]Behavioral differences between the with-statement and ExitStack[/-][+]Behavioral differences between the with-statement and `ExitStack.enter_context`[/+]on Feb 2, 2026
Instead of using callable() I would suggest that maybe we first try to check if the enter/exit methods are descriptors or not by checking the presence of __get__. I think we have similar approaches with partial/partialmethod but I am not entirely sure
This looks correct but I am not very fond of having a huge try-catch here. Likewise it does not seem right to catch the attribute error that could be raised by __get__ so you should move the descriptor handling after the try-catch.
diff --git a/Lib/contextlib.py b/Lib/contextlib.py
index 5b646fabca0..5b5dc48affa 100644
--- a/Lib/contextlib.py+++ b/Lib/contextlib.py@@ -527,6 +527,10 @@ def enter_context(self, cm):
except AttributeError:
raise TypeError(f"'{cls.__module__}.{cls.__qualname__}' object does "
f"not support the context manager protocol") from None
+ if (_enter_get := getattr(_enter, "__get__", None)) is not None:+ _enter = _enter_get(cm, cls)+ if (_exit_get := getattr(_exit, "__get__", None)) is not None:+ _exit = _exit_get(cm, cls)
result = _enter(cm)
self._push_cm_exit(cm, _exit)
return result
Thank you for the suggestion! I updated the change. But I found that the change breaks some contextlib tests:
Running Debug|x64 interpreter...
..................................................EE......E..F..............................
======================================================================
ERROR: test_dont_reraise_RuntimeError (Lib.test.test_contextlib.TestExitStack.test_dont_reraise_RuntimeError)
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\Users\Administrator\Desktop\cpython\main\Lib\test\test_contextlib.py", line 1151, in test_dont_reraise_RuntimeError
es_ctx.enter_context(second())
~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^
File "C:\Users\Administrator\Desktop\cpython\main\Lib\contextlib.py", line 538, in enter_context
result = _enter(cm)
TypeError: _GeneratorContextManager.__enter__() takes 1 positional argument but 2 were given
======================================================================
ERROR: test_enter_context (Lib.test.test_contextlib.TestExitStack.test_enter_context)
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\Users\Administrator\Desktop\cpython\main\Lib\test\test_contextlib.py", line 786, in test_enter_context
stack.enter_context(cm)
~~~~~~~~~~~~~~~~~~~^^^^
File "C:\Users\Administrator\Desktop\cpython\main\Lib\contextlib.py", line 538, in enter_context
result = _enter(cm)
TypeError: TestBaseExitStack.test_enter_context.<locals>.TestCM.__enter__() takes 1 positional argument but 2 were given
======================================================================
ERROR: test_exit_exception_explicit_none_context (Lib.test.test_contextlib.TestExitStack.test_exit_exception_explicit_none_context) (<subtest>)
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\Users\Administrator\Desktop\cpython\main\Lib\test\test_contextlib.py", line 1002, in test_exit_exception_explicit_none_context
with cm():
~~^^
File "C:\Users\Administrator\Desktop\cpython\main\Lib\contextlib.py", line 141, in __enter__
return next(self.gen)
File "C:\Users\Administrator\Desktop\cpython\main\Lib\test\test_contextlib.py", line 996, in my_cm_with_exit_stack
stack.enter_context(my_cm())
~~~~~~~~~~~~~~~~~~~^^^^^^^^^
File "C:\Users\Administrator\Desktop\cpython\main\Lib\contextlib.py", line 538, in enter_context
result = _enter(cm)
TypeError: _GeneratorContextManager.__enter__() takes 1 positional argument but 2 were given
======================================================================
FAIL: test_exit_exception_with_correct_context (Lib.test.test_contextlib.TestExitStack.test_exit_exception_with_correct_context)
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\Users\Administrator\Desktop\cpython\main\Lib\test\test_contextlib.py", line 1055, in test_exit_exception_with_correct_context
stack.enter_context(gets_the_context_right(exc4))
~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\Administrator\Desktop\cpython\main\Lib\contextlib.py", line 538, in enter_context
result = _enter(cm)
TypeError: _GeneratorContextManager.__enter__() takes 1 positional argument but 2 were given
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "C:\Users\Administrator\Desktop\cpython\main\Lib\test\test_contextlib.py", line 1060, in test_exit_exception_with_correct_context
self.assertIs(exc, exc4)
~~~~~~~~~~~~~^^^^^^^^^^^
AssertionError: TypeError('_GeneratorContextManager.__enter__() takes 1 positional argument but 2 were given') is not Exception(4)
----------------------------------------------------------------------
Ran 92 tests in 0.493s
FAILED (failures=1, errors=3)
I found that the __enter__ object defined by a regular class method defination also has a __get__ attribute:
classCM:
def__init__(self):
passdef__enter__(self):
passdef__exit__(self, *exc_info):
passclassSlotsCM:
__slots__= ("__enter__", "__exit__")
def__init__(self):
self.__enter__=self.__exit__=lambda*__: Nonecm=CM()
cls=type(cm)
_enter=cls.__enter__print(getattr(_enter, "__get__", None)) # <method-wrapper '__get__' of function object at 0x00000154BDFB4650>s=SlotsCM()
cls=type(s)
_enter=cls.__enter__print(getattr(_enter, "__get__", None)) # <method-wrapper '__get__' of member_descriptor object at 0x000002C78844D5B0>
I tried to add the callable() check and this passes existing tests:
diff --git a/Lib/contextlib.py b/Lib/contextlib.py
index 5b5dc48affa..fbc4de9277b 100644
--- a/Lib/contextlib.py+++ b/Lib/contextlib.py@@ -527,10 +527,14 @@ def enter_context(self, cm):
except AttributeError:
raise TypeError(f"'{cls.__module__}.{cls.__qualname__}' object does "
f"not support the context manager protocol") from None
+ if not callable(_enter) and hasattr(_enter, "__get__"):+ _enter = _enter.__get__(cm, cls)+ if not callable(_exit) and hasattr(_exit, "__get__"):+ _exit = _exit.__get__(cm, cls)
result = _enter(cm)
self._push_cm_exit(cm, _exit)
return result
Urgh, I hate working with descriptors. I actually wonder what we did on the C side for __enter__ as a slot to work. This may require more subtle checks
Should we make the equivalent code part of with-statement document a bit more precise? Like:
diff --git a/Doc/reference/compound_stmts.rst b/Doc/reference/compound_stmts.rst
index cc9f56377ba..d7b390cc565 100644
--- a/Doc/reference/compound_stmts.rst+++ b/Doc/reference/compound_stmts.rst@@ -541,7 +541,7 @@ The following code::
with EXPRESSION as TARGET:
SUITE
-is semantically equivalent to::+is roughly equivalent to::
manager = (EXPRESSION)
enter = type(manager).__enter__
@@ -564,6 +564,8 @@ is semantically equivalent to::
if not hit_except:
exit(manager, None, None, None)
+except that the actual special method lookup sequence handles the situation where ``__enter__`` and ``__exit__`` are member descriptors defined by ``__slots__``.+
Activity
tanloong commentedon Feb 2, 2026
contextlib.ExitStack.enter_context(cm) calls
type(cm).__enter__(cm). The problem looks like thattype(cm)does not recognize those__enter__and__exit__methods defined by__slots__. The document saystype()generally returns the same object as returned byobject.__class__, which throws the same TypeError:gottadiveintopython commentedon Feb 2, 2026
I don't fully understand the error message, but yes, the cause of the issue seems to be that
(Async)ExitStack.enter_(async)_context()looks up dunder methods on the class rather than onthe instance.
tanloong commentedon Feb 2, 2026
I am not familiar with the
member_descriptorneither, it looks like a concept in C. A printing shows it refers to the__enter__object reserved by__slots__.Also I found that calling
__get__(s)on the member descriptor returns the same function object ass.__enter__.I tried to modify the
contextlib.ExitStack.enter_context()code to uses.__class__.__enter__.__get__(s). It works, theTypeErroris gone:tanloong commentedon Feb 2, 2026
I just found that there is also a difference between the actual with-statement behavior and its documented semantically equivalent code. The documentted equivalent code throws the same
TypeErrorascontextlib.ExitStack.enter_context().PEP 343 explains
withstatement with generally the same code which also throws theTypeErrorin this usecase.[-]Behavioral differences between the with-statement and ExitStack[/-][+]Behavioral differences between the with-statement and `ExitStack.enter_context`[/+]picnixz commentedon Feb 2, 2026
Instead of using callable() I would suggest that maybe we first try to check if the enter/exit methods are descriptors or not by checking the presence of
__get__. I think we have similar approaches with partial/partialmethod but I am not entirely suretanloong commentedon Feb 2, 2026
@picnixz Thanks for the suggestion! This is the modified change I made following the partialmethod.
picnixz commentedon Feb 2, 2026
This looks correct but I am not very fond of having a huge try-catch here. Likewise it does not seem right to catch the attribute error that could be raised by
__get__so you should move the descriptor handling after the try-catch.tanloong commentedon Feb 2, 2026
OK here is the new change:
picnixz commentedon Feb 2, 2026
Yes, something like that. Though I think you can write something more readable as:
tanloong commentedon Feb 2, 2026
Thank you for the suggestion! I updated the change. But I found that the change breaks some contextlib tests:
I found that the
__enter__object defined by a regular class method defination also has a__get__attribute:I tried to add the
callable()check and this passes existing tests:picnixz commentedon Feb 2, 2026
Urgh, I hate working with descriptors. I actually wonder what we did on the C side for
__enter__as a slot to work. This may require more subtle checkstanloong commentedon Feb 2, 2026
I am not good at C but
dis.dis()shows there is aLOAD_SPECIALto handle the special methods.11 remaining items
tanloong commentedon Feb 3, 2026
Should we make the equivalent code part of with-statement document a bit more precise? Like:
gh-144386: Add support for descriptors in ExitStack and AsyncExitStack (
pythongh-144386: Update equivalent code for "with", "async with" and …
serhiy-storchaka commentedon Feb 4, 2026
Yes, this is a separate PR #144472 (the documentation changes will be backported).
pythongh-144386: Add support for descriptors in ExitStack and AsyncEx…
gh-144386: Update equivalent code for "with", "async with" and "async…
pythongh-144386: Update equivalent code for "with", "async with" and …
pythongh-144386: Update equivalent code for "with", "async with" and …
[3.14] gh-144386: Update equivalent code for "with", "async with" and…
[3.13] gh-144386: Update equivalent code for "with", "async with" and…
pythongh-144386: Update equivalent code for "with", "async with" and …
pythongh-144386: Add support for descriptors in ExitStack and AsyncEx…
pythongh-144386: Update equivalent code for "with", "async with" and …
serhiy-storchaka commentedon May 29, 2026
See also #150484. The old behavior caused a bug in
mock.mock_open().