Skip to content
Advertisement

Why doesn’t contextmanager reraise exception?

I want to replace class’s __enter__/__exit__ functions with single function decorated as contextlib.contextmanager, here’s code:

class Test:
    def __enter__(self):
        self._cm_obj = self._cm()
        self._cm_obj.__enter__()

    def __exit__(self, exc_type, exc_val, exc_tb):
        try:
            # Here we first time caught exception,
            # Pass it to _cm:
            self._cm_obj.__exit__(exc_type, exc_val, exc_tb)
        except:
            # Here we should catch exception reraised by _cm,
            # but it doesn't happen.
            raise
        else:
            return True

    @contextmanager
    def _cm(self):
        print('enter')
        try:
            yield
        except:
            # Here we got exception from __exit__
            # Reraise it to tell __exit__ it should be raised.
            raise
        finally:
            print('exit')


with Test():
    raise Exception(123)

When we got exception inside Test’s __exit__, we pass it to _cm‘s __exit__, it works fine, I see exception inside _cm. But then, when I decide to reraise inside _cm, it doesn’t happen: after Test’s __exit__ I don’t see exception (and code works wrong, without exception).

Why doesn’t exeprion reraised inside __exit__?

If it’s normal behavior, could you advice me solution to properly replace __enter__/__exit__ with contextmanager function?

Edit:

cm_obj.__exit__ returns None on exception inside _cm and True if exception was suppressed (or not raised at all).

At other side, inside Test.__exit__ we can return None to propagate current exception or True to suppress it.

Looks like just returning cm_obj.__exit__‘s value inside Test.__exit__do job, this code working as I want:

class Test:
    def __enter__(self):
        self._cm_obj = self._cm()
        self._cm_obj.__enter__()

    def __exit__(self, exc_type, exc_val, exc_tb):
        return self._cm_obj.__exit__(exc_type, exc_val, exc_tb)

    @contextmanager
    def _cm(self):
        print('---- enter')
        try:
            yield
        except:
            raise  # comment to suppess exception
            pass
        finally:
            print('---- exit')


with Test():
    raise Exception(123)

Advertisement

Answer

The exception is not raised in __exit__, so there is nothing to reraise.

The exception is passed in to the method as arguments, because you can’t raise an exception in another method (other than a generator). You don’t have to raise the exception either, you need to simply return not return a true value for the exception to be propagated. Returning None should suffice.

From the With Statement Context Managers documentation:

Exit the runtime context related to this object. The parameters describe the exception that caused the context to be exited. If the context was exited without an exception, all three arguments will be None.

If an exception is supplied, and the method wishes to suppress the exception (i.e., prevent it from being propagated), it should return a true value. Otherwise, the exception will be processed normally upon exit from this method.

Note that __exit__() methods should not reraise the passed-in exception; this is the caller’s responsibility.

Bold emphasis mine.

You instead suppress the result of the self._cm_obj.__exit__() call, and return True to ensure the exception isn’t raised again:

def __exit__(self, exc_type, exc_val, exc_tb):
    self._cm_obj.__exit__(exc_type, exc_val, exc_tb)
    return True

If you didn’t return True here you’d see the exception re-raised by the with statement.

You can’t re-raise exceptions that don’t reach the context manager however. If you catch and handle an exception in the code block (inside _cm for example), then the context manager will never be told about this. Suppressed exceptions stay suppressed.

Note that @contextmanager can’t change these rules; although it transfers the exception into the generator with generator.throw(), it also has to catch that same exception if your generator doesn’t handle it. It’ll instead return a false value at that moment, because that is what a contextmanager.__exit__() method should do.

Advertisement