Following the Python TDD study track, I came across an older reference(but still relevant) explanation of what a context manager is. I will do a full write up when I have a chance.
Python 2.5 : The ‘with’ statement
== Have to spend a whole day to rewrite this explanation ==
Writing Context Managers
Under the hood, the ‘with‘ statement is fairly complicated. Most people will only use ‘with‘ in company with existing objects and don’t need to know these details, so you can skip the rest of this section if you like. Authors of new objects will need to understand the details of the underlying implementation and should keep reading.
A high-level explanation of the context management protocol is:
- The expression is evaluated and should result in an object called a
context manager”. The context manager must have __enter__() and __exit__() methods.
- The context manager’s __enter__() method is called. The value returned is assigned to VAR. If no
'as VAR' clause is present, the value is simply discarded.
- The code in BLOCK is executed.
- If BLOCK raises an exception, the __exit__(type, value, traceback) is called with the exception details, the same values returned by sys.exc_info(). The method’s return value controls whether the exception is re-raised: any false value re-raises the exception, and
True will result in suppressing it. You’ll only rarely want to suppress the exception, because if you do the author of the code containing the ‘with‘ statement will never realize anything went wrong.
- If BLOCK didn’t raise an exception, the __exit__() method is still called, but type, value, and traceback are all
Let’s think through an example. I won’t present detailed code but will only sketch the methods necessary for a database that supports transactions.
(For people unfamiliar with database terminology: a set of changes to the database are grouped into a transaction. Transactions can be either committed, meaning that all the changes are written into the database, or rolled back, meaning that the changes are all discarded and the database is unchanged. See any database textbook for more information.)
Let’s assume there’s an object representing a database connection. Our goal will be to let the user write code like this:
db_connection = DatabaseConnection()
with db_connection as cursor:
cursor.execute('insert into ...')
cursor.execute('delete from ...')
# ... more operations ...
The transaction should be committed if the code in the block runs flawlessly or rolled back if there’s an exception. Here’s the basic interface for DatabaseConnection that I’ll assume:
# Database interface
def cursor (self):
"Returns a cursor object and starts a new transaction"
def commit (self):
"Commits current transaction"
def rollback (self):
"Rolls back current transaction"
The __enter__() method is pretty easy, having only to start a new transaction. For this application the resulting cursor object would be a useful result, so the method will return it. The user can then add
as cursor to their ‘with‘ statement to bind the cursor to a variable name.
def __enter__ (self):
# Code to start a new transaction
cursor = self.cursor()
The __exit__() method is the most complicated because it’s where most of the work has to be done. The method has to check if an exception occurred. If there was no exception, the transaction is committed. The transaction is rolled back if there was an exception.
In the code below, execution will just fall off the end of the function, returning the default value of
None is false, so the exception will be re-raised automatically. If you wished, you could be more explicit and add a return statement at the marked location.
def __exit__ (self, type, value, tb):
if tb is None:
# No exception, so commit
# Exception occurred, so rollback.
# return False