Calling __enter__ and __exit__ manually

Your first example is not a good idea:

  1. What happens if slave_connection.__enter__ throws an exception:

    • master_connection acquires its resource
    • slave_connection fails
    • DataSync.__enter__ propogates the exception
    • DataSync.__exit__ does not run
    • master_connection is never cleaned up!
    • Potential for Bad Things
  2. What happens if master_connection.__exit__ throws an exception?

    • DataSync.__exit__ finished early
    • slave_connection is never cleaned up!
    • Potential for Bad Things

contextlib.ExitStack can help here:

def __enter__(self):
    with ExitStack() as stack:
        stack.enter_context(self.master_connection)
        stack.enter_context(self.slave_connection)
        self._stack = stack.pop_all()
    return self

def __exit__(self, exc_type, exc, traceback):
    self._stack.__exit__(self, exc_type, exc, traceback)

Asking the same questions:

  1. What happens if slave_connection.__enter__ throws an exception:

    • The with block is exited, and stack cleans up master_connection
    • Everything is ok!
  2. What happens if master_connection.__exit__ throws an exception?

    • Doesn’t matter, slave_connection gets cleaned up before this is called
    • Everything is ok!
  3. Ok, what happens if slave_connection.__exit__ throws an exception?

    • ExitStack makes sure to call master_connection.__exit__ whatever happens to the slave connection
    • Everything is ok!

There’s nothing wrong with calling __enter__ directly, but if you need to call it on more than one object, make sure you clean up properly!

Leave a Comment