Context managers are awesome

I love context managers, and I love the with keyword. If you’ve never dealt with context managers or with, here’s a practical guide which explains how to use them. You may also read the more official PEP 343 which introduced these features to the language.

Using with and context managers in your code contributes a lot to making your code more beautiful and maintainable. Every time you replace a try-finally clause with a with clause, an angel gets a pair of wings.

Now, you don’t need any official ContextManager class in order to use context managers or define them; you just need to define __enter__() and __exit__() methods in your class, and then you can use your class as a context manager. But, if you use the ContextManager class as a base class to your context manager class, you could enjoy a few more features that might make your code a bit more concise and elegant.

What does ContextManager add?

The ContextManager class allows using context managers as decorators (in addition to their normal use) and supports writing context managers in a new form called manage_context(). (As well as the original forms). First let’s import:

>>> from python_toolbox import context_management

Now let’s go over the features one by one.

The ContextManager class allows you to define context managers in new ways and to use context managers in new ways. I’ll explain both of these; let’s start with defining context managers.

Defining context managers

There are 3 different ways in which context managers can be defined, and each has their own advantages and disadvantages over the others.

  • The classic way to define a context manager is to define a class with __enter__() and __exit__() methods. This is allowed, and if you do this you should still inherit from ContextManager. Example:

    >>> class MyContextManager(context_management.ContextManager):
    ...     def __enter__(self):
    ...         pass # preparation
    ...     def __exit__(self, type_=None, value=None, traceback=None):
    ...         pass # cleanup
  • As a decorated generator, like so:

    >>> @context_management.ContextManagerType
    ... def MyContextManager():
    ...     # preparation
    ...     try:
    ...         yield
    ...     finally:
    ...         pass # cleanup

    The advantage of this approach is its brevity, and it may be a good fit for relatively simple context managers that don’t require defining an actual class. This usage is nothing new; it’s also available when using the standard library’s contextlib.contextmanager() decorator. One thing that is allowed here that contextlib doesn’t allow is to yield the context manager itself by doing yield context_management.SelfHook.

  • The third and novel way is by defining a class with a manage_context() method which returns a decorator. Example:

    >>> class MyContextManager(ContextManager):
    ...     def manage_context(self):
    ...         do_some_preparation()
    ...         with other_context_manager:
    ...             yield self

This approach is sometimes cleaner than defining __enter__() and __exit__(); especially when using another context manager inside manage_context(). In our example we did with other_context_manager in our manage_context(), which is shorter, more idiomatic and less double-underscore-y than the equivalent classic definition:

>>> class MyContextManager(object):
...         def __enter__(self):
...             do_some_preparation()
...             other_context_manager.__enter__()
...             return self
...         def __exit__(self, *exc):
...             return other_context_manager.__exit__(*exc)

Another advantage of the manage_context() approach over __enter__() and __exit__() is that it’s better at handling exceptions, since any exceptions would be raised inside manage_context() where we could except them, which is much more idiomatic than the way __exit__() handles exceptions, which is by receiving their type and returning whether to swallow them or not.

These were the different ways of defining a context manager. Now let’s see the different ways of using a context manager:

Using context managers

There are 2 different ways in which context managers can be used:

  • The plain old honest-to-Guido with keyword:

    >>> with MyContextManager() as my_context_manager:
    ...     do_stuff()
  • As a decorator to a function:

    >>> @MyContextManager()
    ... def do_stuff():
    ...     pass # doing stuff

    When the do_stuff function will be called, the context manager will be used. This functionality is also available in the standard library of Python 3.2+ by using contextlib.ContextDecorator, but here it is combined with all the other goodies given by ContextManager. Another advantage that ContextManager has over contextlib.ContextDecorator is that it uses Michele Simionato’s excellent decorator module to preserve the decorated function’s signature.

That’s it. Inherit all your context managers from ContextManager (or decorate your generator functions with ContextManagerType) to enjoy all of these benefits.