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 commented on Feb 2, 2026
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:
gottadiveintopython commented on 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 on
the instance.
tanloong commented on Feb 2, 2026
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 tried to modify the contextlib.ExitStack.enter_context() code to use s.__class__.__enter__.__get__(s). It works, the TypeError is gone:
tanloong commented on 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 TypeError as contextlib.ExitStack.enter_context().
PEP 343 explains with statement with generally the same code which also throws the TypeError in this usecase.
picnixz commented 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
tanloong commented on Feb 2, 2026
@picnixz Thanks for the suggestion! This is the modified change I made following the partialmethod.
picnixz commented on 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 commented on Feb 2, 2026
OK here is the new change:
picnixz commented on Feb 2, 2026
Yes, something like that. Though I think you can write something more readable as:
tanloong commented on Feb 2, 2026
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:
I tried to add the callable() check and this passes existing tests:
picnixz commented on 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 checks
tanloong commented on Feb 2, 2026
I am not good at C but dis.dis() shows there is a LOAD_SPECIAL to handle the special methods.
11 remaining items
tanloong commented on 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 commented on 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 commented on May 29, 2026
See also #150484. The old behavior caused a bug in mock.mock_open().