Context Managers and the “with” statement in Python

Python 2.5 introduced the with statement, and for some time I wondered what the hell it was. There was no clear-cut description of what the inclusion of that statement into the language was meant to accomplish. Well, 3 minor versions and 1 major version of python later, I think I’ve got the hang of it.

In this post, I’ll try to explain why the “with” statement is a good idea, its syntax, and hopefully some meaningful examples.

Motivation

The “with” statement is meant to simplify the code for some common use-cases of the setup..try..except..finally idiom. Consider a simple example, a database transaction.

  1. First, you need to establish a connection to the database. This needs to be done regardless of what exactly you intend to do with the database.
  2. Secondly, you’re going to do some transaction processing. This is the actual logic you’ll be implementing.
  3. In the case of an exceptional circumstance or occurrence, you’ll want to roll back the database to the state it was in before the transaction.
  4. Finally, irrespective of the outcome of the transaction, some cleanup activities may need to be performed. This may include closing the database connection, and logging the transaction.

This sequence of activities may be put quite succinctly into code:

do_setup()

try :
    do_task()

except SomeError :
    handle_the_error()

finally :
    do_cleanup()

This common scenario is what the “with” statement is concerned with, and has to do with the concept of Context Managers.

Context Managers, strictly speaking, are objects with two special methods, __enter__ and __exit__. However, these methods are key to achieving this transaction-type behaviour.

Syntax

The syntax of the “with” statement is:

with context_manager as variable :
    do_stuff()

When this block is encountered, the following things happen:

  1. The __enter__ method of the context_manager is called. This can do any setup it wants to do, and may return something. If a name is given following the context manager as it has been here in as variable, then the return value of the __enter__ method is assigned to it. Otherwise the return value is discarded.
  2. The block is executed.
  3. When the block exits, the __exit__ method of the context_manager is called. The block can exit in a number of different ways:
    • An exception could have been raised. If this is the case, the __exit__ method is passed the exception type, value, and the traceback.
    • The block completed normally. Yippee. The __exit__ method is passed three arguments equal to None.
  4. If the block exited because of an exception, the return value of the __exit__ method is considered. If the return value is False, the exception is reraised. If it is True, the exception is suppressed and execution continues normally after the block.
  5. On the other hand, if the block does not exit because of an exception, any return value is ignored.

Writing your own (very simple) Context Manager

You could of course, create a class that implemented the above mentioned two methods, but along with the “with” statement, Python 2.5 also introduced the contextlib standard library module, that makes the task of writing your own context managers somewhat easier.

The example I’m going to take is that of temporarily changing the working directory of a script. Say you need to step out of the current directory and into another one, do some work, and go back to where you were before. Here’s a very simple context manager to do just that:

import contextlib
import os

@contextlib.contextmanager
def step_out(new_directory) :
    current_directory = os.getcwd()
    os.chdir(new_directory)
    try :
        yield
    finally :
        os.chdir(current_directory)

That should all be fairly self explanatory, as it fits into the same 4 steps I gave before. The only detail needing elaboration is the single yield statement. Basically, this is where the context manager yields control to the block, and where control resumes when the block ends.

So far, I haven’t come across the need for anything more complicated than that, although the database example I mentioned would definitely require some more grunt work. That being the case, I assume implementing the context manager from scratch with __enter__ and __exit__ would be the way to go.

Freebies: Handy builtin contexts.

There are some builtin contexts that you can use without any hassle.

Files

with open('foo.txt') as file :
    do_stuff_with_file(file)

That will take care of automatically closing the file for you when you leave. However, the file won’t hang around after the block ends.

Precision in Decimals

import decimal

with decimal.localcontext() as context :
    context.prec = 50
    print(decimal.Decimal('2').sqrt())
print(decimal.Decimal('2').sqrt())

Running this gives me:

1.4142135623730950488016887242096980785696718753769
1.414213562373095048801688724

Do you have any handy context managers you use? Let me know!

Respond

Comments for this entry have been closed.