Looking at the bytecode for a simple
with statement, you will notice there are a lot of opcodes being used. Most of it is managing the execution stack for the interpreter and dealing with potential exceptions. As such, I'm going to skip covering the bytecode and instead go with the unravelled syntax as provided by the language reference (touched up for easier reading):
To help illustrate how this all works, we are going to use the classic RAII lock example throughout this blog post. If you're unfamiliar with RAII, it stands for "resource acquisition is initialization". It basically means that by creating an object something happens, and by freeing/deleting the object that something is undone. For locks it means allocating the lock upon creation and then releasing the lock upon deletion. Because Python as a language has no guaranteed cleanup semantics in terms of time and order, context managers were introduced to explicitly bring a language construct to Python that lets us get the benefits of the RAII concept.
There are two parts to a context manager. One is calling its
__enter__ special method when entering the
with block; for our locking example, this is calling
threading.Lock.acquire(). If you use the
as clause of a context manager, the object the
__enter__ method returns is what gets bound to the variable you specified.
The second part of a context manager is the
__exit__ special method when the
with block is exited; this is calling
threading.Lock.release() in our locking example. The key detail about
__exit__ involves whether an exception was raised in the body of the
with block. If an exception was raised, it is passed in by its constituent parts as returned by
__exit__; if no exception is raised then
None is given for those three parts instead. If an exception happens to be raised in the body of the context manager, the
__exit__ method can choose to either suppress or allow the exception to continue to propagate. Which action is taken is controlled by whether a true or false value is returned by the method, respectively. The meaning of the return value is this way so that exceptions will be re-raised thanks to the fact that methods by default return
If you would like more historical details, PEP 343 is what brought context managers to Python.