14. Context Manager

The concept of a context manager is to manage resources. Typically, these resources are limited, expensive to acquire or the references to such resources need to be released (so others may also access the resources). Examples of resources that we might want to manage include connections to databases or references to files. The context manager’s main job is to manage the lifecycle of the resource, which is as follows.

  • initialization (setup): this phase is for the context manager to set up the resource

  • enter (reference and execution): this phase is to acquire the reference to the resource and give it back to the user for use

  • exit (tear down): this phase is for the context manager to clean up (release) the resource.

If a context manager is being used, Python provides the with idiom to express the use of a resource.

with <context_manager> as <resource>:
    # do something with the resource

14.1. Basic

A context manager is defined by creating a class and implementing at least two methods.

  • __enter__()

  • __exit__()

 1class HelloManager(object):
 2    def __init__(self):
 3        print('hello, world!')
 4
 5    def __enter__(self):
 6        print('how are you doing?')
 7        return 7
 8
 9    def __exit__(self, exc_type, exc_value, exc_traceback):
10        print('bye, world!')
11
12with HelloManager() as hello_manager:
13    print(f'what did HelloManager return? {hello_manager}')

Note the method signature of __exit__(self, exc_type, exc_value, exc_traceback) which has several arguments/parameters. These arguments might be useful when there are exceptions. The arguments are named in a short way but more useful names could be mapped as follows.

  • exc_type to exception_type

  • exc_value to exception_value

  • exc_traceback to traceback

These arguments are not optional when defining __exit__(); they must be part of the method signature or Python will assume that we are not implementing a cleanup for the exit phase.

14.1.1. Exercise

Create a context manager that generates the width and length randomly for a rectangle. Try using that context manager.

Solution.

 1from random import randint
 2
 3class RandomRectManager(object):
 4    def __init__(self, a, b):
 5        self.a = a
 6        self.b = b
 7
 8    def __enter__(self):
 9        width = randint(self.a, self.b)
10        length = randint(self.a, self.b)
11        return {
12            'width': width,
13            'length': length
14        }
15
16    def __exit__(self, etype, eval, etrace):
17        pass
18
19with RandomRectManager(1, 10) as rect:
20    print(f'what did RandomRectManager return? {rect}')

14.1.2. Exercise

Create a context manager that generates the width and length randomly for 10 rectangle. Try using a generator function to help you.

Solution.

 1from random import randint
 2
 3class RandomRectManager(object):
 4    def __init__(self, a, b):
 5        self.a = a
 6        self.b = b
 7
 8    def __enter__(self):
 9        return self.__get_rect()
10
11    def __exit__(self, etype, eval, etrace):
12        pass
13
14    def __get_rect(self):
15        n = 0
16        while n < 10:
17            n += 1
18
19            width = randint(self.a, self.b)
20            length = randint(self.a, self.b)
21
22            yield {
23                'width': width,
24                'length': length
25            }
26
27with RandomRectManager(1, 10) as rects:
28    for rect in rects:
29        print(rect)

14.2. Functions as context managers

We can also annotate functions with the @contextmanager decorator to make them context manager. Here’s an example below where we decorate get_rects() to be a context manager. See how we use try-except to manage the lifecycle of the context manager? Also, there should be only one yield.

 1from random import randint
 2from contextlib import contextmanager
 3
 4@contextmanager
 5def get_rects(a, b):
 6    print('initialization goes here')
 7    try:
 8        yield ({'width': randint(a, b), 'height': randint(a, b)} for _ in range(10))
 9    except:
10        print('exception handling goes here')
11    finally:
12        print('clean up goes here')
13
14with get_rects(1, 10) as rects:
15    for rect in rects:
16        print(rect)

You can use multiple context managers in tandem as follows.

 1from random import randint
 2from contextlib import contextmanager
 3
 4@contextmanager
 5def get_rects(a=1, b=10, n=10):
 6    try:
 7        yield ({'width': randint(a, b), 'height': randint(a, b)} for _ in range(n))
 8    except:
 9        pass
10    finally:
11        pass
12
13@contextmanager
14def get_tris(a=1, b=10, n=10):
15    try:
16        yield ({'base': randint(a, b), 'height': randint(a, b)} for _ in range(n))
17    except:
18        pass
19    finally:
20        pass
21
22# avoid nested with if you can
23with get_rects() as rects:
24    with get_tris() as tris:
25        for rect, tri in zip(rects, tris):
26            print(rect, ' | ', tri)
27
28# you can reference multiple context managers
29with get_rects(1, 10) as rects, get_tris(1, 10) as tris:
30    for rect, tri in zip(rects, tris):
31        print(rect, ' | ', tri)

14.2.1. Exercise

Write a function that can be used as a context manager to generate any number of radiuses. Make sure you parameterize the range of randiuses and the number of radiuses to generate.

Solution.

 1from random import randint
 2from contextlib import contextmanager
 3
 4@contextmanager
 5def get_circles(a=1, b=10, n=20):
 6    try:
 7        yield ({'radius': randint(a, b)} for _ in range(n))
 8    except:
 9        pass
10    finally:
11        pass
12
13with get_circles(5, 50, 10) as circles:
14    for c in circles:
15        print(c)