Skip to content
Advertisement

Why does calling the __exit__ method on unittest.mock._patch throw an IndexError?

When I define a function and patch it using the with statement it runs fine.

def some_func():
  print('this is some_func')

with patch('__main__.some_func', return_value='this is patched some_func'):
  some_func()

Output:

this is patched some_func

My understanding is that using the with statement would cause the __enter__ and __exit__ methods to be called on the patch object. So I thought that would be equivalent to doing this:

patched_some_func = patch('__main__.some_func', return_value='this is patched some_func')
patched_some_func.__enter__()
some_func()
patched_some_func.__exit__()

The output from the some_func call is the same in this case:

this is patched some_func

But I get an IndexError when I call the __exit__ method:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib64/python3.7/unittest/mock.py", line 1437, in __exit__
    return exit_stack.__exit__(*exc_info)
  File "/usr/lib64/python3.7/contextlib.py", line 482, in __exit__
    received_exc = exc_details[0] is not None
IndexError: tuple index out of range

Why am I getting an IndexError in the second case but not the first where I use with? (An additional note, I get this error on python 3.7.10 but not on python 3.7.3)

Advertisement

Answer

Your understanding is incorrect; on exit the context manager __exit__ method is passed None, None, None:

patched_some_func.__exit__(None, None, None)

The arguments are always passed in, either the exception information, or None for each of the three arguments. See the documentation on object.__exit__():

If the context was exited without an exception, all three arguments will be None.

Also see the PEP 343 – The “with” Statement specification section:

The calling convention for mgr.__exit__() is as follows. If the finally-suite was reached through normal completion of BLOCK or through a non-local goto (a break, continue or return statement in BLOCK), mgr.__exit__() is called with three None arguments. If the finally-suite was reached through an exception raised in BLOCK, mgr.__exit__() is called with three arguments representing the exception type, value, and traceback.

As to why you get an index error in 3.7.10: the mock.patch() implementation in older releases used an incorrect implementation that could result in an unexpected exception, and the fix for this was to use an contextlib.ExitStack() instance. The ExitStack context manager uses def __exit__(self, *exc_details): as the method signature and expects the exc_details tuple to have at least 1 element in it, which normally is either None or an exception object. The bug fix is part of Python 3.7.8, which is why you don’t see the same context manager in 3.7.3 as you see in 3.7.10. In the older version of the code, the patcher __exit__() method also uses a catchall *args tuple, but otherwise doesn’t try to index into this tuple.

Advertisement