Why does my context manager not exit on exception

Question:

I am learning about context managers and was trying to build one myself. The following is a dummy context manager that opens a file in read mode (I know I can just do with open(...): .... this is just an example I built to help me understand how to make my own context managers):

@contextmanager
def open_read(path: str):
    f = open(path, 'r')
    print('open')
    yield f
    f.close()
    print('closed')


def foo():
    try:
        with open_read('main.py') as f:
            print(f.readline())
            raise Exception('oopsie')
    except Exception:
        pass
    print(f.readline())


foo()

I expect this code to print:

open
<line 1 of a.txt>
closed
ValueError: I/O operation on closed file.

But instead it prints:

open
<line 1 of a.txt>
<line 2 of a.txt>

It didn’t close the file!

This seems to contradict python’s docs which state that __exit__ will be called whether the with statement exited successfully or with an exception:

object.exit(self, exc_type, exc_value, traceback)

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.

Interestingly, when I reimplemented the context manager as shown below, it worked as expected:

class open_read(ContextDecorator):
    def __init__(self, path: str):
        self.path = path
        self.f = None

    def __enter__(self):
        self.f = open(self.path, 'r')
        print('open')
        return self.f

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.f.close()
        print('closed')

Why didn’t my original implementation work?

Asked By: Eyal Kutz

||

Answers:

The line f.close() is never reached (we exit that frame early due to unhandled exception), and then the exception was "handled" in the outer frame (i.e. within foo).

If you want it to close regardless, you’ll have to implement it like that:

@contextmanager
def open_read(path: str):
    f = open(path, 'r')
    try:
        print('open')
        yield f
    finally:
        f.close()
        print('closed')

However, I’d like to point out that that the built-in open is already returning a context-manager, and you may be reinventing stdlib contextlib.closing.

Answered By: wim