5

I wrote this example to show myself that __exit__ is not being run when an exception occurs:

class A(object):
    def __enter__(self):
        print('enter')
    def __exit__(self):
        print('exit')
try:
    with A() as a:
        raise RunTimeError()
except Exception as e:
    print('except')

Output:

enter
except

That said, what is the correct way to use a with statement and catch exceptions, while making sure __exit__ is being run in the end? Thanks!

tobias_k
  • 81,265
  • 12
  • 120
  • 179
Idan
  • 5,365
  • 5
  • 24
  • 28
  • 3
    The errors first pass through `__exit__` your `__exit__` functions needs to have parameters `exc_type, exc_value, traceback` (https://docs.python.org/3/reference/datamodel.html#object.__exit__). – Willem Van Onsem Dec 22 '17 at 10:25
  • maybe this helps you https://stackoverflow.com/questions/713794/catching-an-exception-while-using-a-python-with-statement – Nicolas Heimann Dec 22 '17 at 10:25
  • Can you swap the `try` and `with` clauses order? – Adirio Dec 22 '17 at 10:26
  • Thanks Willem, that's the answer... My bad! – Idan Dec 22 '17 at 10:29
  • Now that's strange: As-is, `exit` is not called, but if I change it to `exit(self, *args)`, it is called (as suggested by @WillemVanOnsem). BUT if I swap the order of `with` and `try`, such that (in my understanding) the `try` should handle the exception without ever bothering the surrounding `with`, then it complains about `exit` having the wrong number of arguments. Why does it only complain about missing arguments then? – tobias_k Dec 22 '17 at 10:36
  • 1
    BTW, that code does not actually raise a `RuntimeError` but a `NameError` :-P – tobias_k Dec 22 '17 at 10:37
  • 1
    @tobias_k: `__exit__` is called, but it results in a `TypeError` (since the parameters do not match) and that type error is then handled in the `except` block. – Willem Van Onsem Dec 22 '17 at 10:37
  • 1
    @WillemVanOnsem Ah, of course, that makes sense. Lesson learned: Never just print "except" without showing the cause. – tobias_k Dec 22 '17 at 10:38

1 Answers1

3

The __exit__ function is called, regardless whether the with body raises errors or not. Your function needs to have additional parameters exc_type (the type of the exception), exc_value (the exception object), and traceback (the traceback that is generated).

In case the with body did not raise an Error, the three parameters are None. In case there is an error, they take the values described above.

But you can for instance close a file, regardless whether there is an error and then later handle the error.

So we can here implement it for instance as:

class A(object):
    def __enter__(self):
        self.file = open('some_file.txt')
        return self.file

    def __exit__(self, exc_type, exc_value, traceback):
        print(('exit', exc_type, exc_value, traceback))
        # close the file, regardless of exceptions
        self.file.close()
        return False  # silence the exception?

If we now write something like:

with A():
    raise Exception

We will obtain the exception, the __exit__ function will print:

('exit', <class 'Exception'>, Exception(), <traceback object at 0x7fc512c924c8>)

We can inspect the exception class, exception value and traceback, and handle accordingly. For instance based on the exception, we might decide to close a file, send a bug report, abort the SQL transaction or not.

The __exit__ function also has a return value (if not specified, a Python function returns None). In case the __exit__ function returns an object with truthiness True, it will surpress the exception: the exception will not raise out of the with block. Otherwise it will be raised out of the with block. For instance in our example, we still receive the exception.

class SilenceExceptions(object):
    def __enter__(self):
        pass

    def __exit__(self, exc_type, exc_value, traceback):
        return True  # silence ALL exceptions

If we now call:

with SilenceExceptions():
    raise Error

we will not see the exception, since it is "catched" in the __exit__ function.

Willem Van Onsem
  • 443,496
  • 30
  • 428
  • 555
  • Thanks. That said, would you say, in this case, that my try-with structure is the correct and usual way of doing this? (And not, for example, with the try-with order reversed)? – Idan Dec 22 '17 at 10:49
  • 2
    That depends whether you want to catch exceptions *within* the block. For instance if you process a file, and you want to ignore the lines that fail to parse, the `try:` should be placed in the `with:` (and in a `for` loop in the `with`). In case you want to close the context if the error raises, then it is common to place the `with` in the `try`. Since you then offer the context amanger the change to solve the problem at context level. – Willem Van Onsem Dec 22 '17 at 10:51