After publishing my last article, which was about list.sort() and sorted(list), I was asked, why I was using the boxx library instead of built-in functionalities to measure the execution time of certain pieces of code. I responded, that it’s only personal preference and that you can simply create your own context manager measuring the execution time of code pieces.

In this article I will show you how to create your own timing context manager. Furthermore, different ways to accomplish that are covered.

Time Measurement

What’s already available?

Python provides you different ways to measure the execution time. For instance you can use Python’s built-in timeit module to measure the execution time of small pieces of code.


>>> import timeit
>>> timeit.timeit('"-".join(str(n) for n in range(100))', number=10000)
0.3018611848820001

However, the timeit.timeit function only accepts strings. This can be quite annoying if you want to measure larger functions. The following example is from the official Python documentation and shows you, how you can run and measure functions using the timeit module.


def test():
    """Stupid test function"""
    L = [i for i in range(100)]


if __name__ == '__main__':
    import timeit
    print(timeit.timeit("test()", setup="from __main__ import test"))

Although it works, it does not look really pythonic.

Another way to measure the execution time is to make use of Python’s built-in cProfile module. However, this is not recommended! It’ s simply a workaround to get a sense of how long certain code pieces need to be executed. In fact it’s not really precise. You can use it via:


>>> python -m cProfile <file_name.py>

Now that we have seen a recommended but unpythonic, and a discredited and unpythonic way, how can we implement our own recommended and pythonic solution?

The Idea

The idea is quite simple: We take the time at the beginning of the execution and subtract it from the time at the end of the execution. Fortunately, Python has a built-in module we can use: time .


>>> import time
>>> start = time.time()
>>> # do some stuff
>>> end = time.time()
>>> print(f"Elapsed Time: {end - start}")

Great! However, adding one line of code before and two lines after a code piece we want to measure is an overhead I don’t want to have. So let’s create a context manager for that.

Creating A Context Manager

There exist different ways to create a context manager in Python. We will have a look at two ways to accomplish that: A class-based and a generator-based context manager.

Class-based Context Manager

To create a class-based context manager, the dunder methods __enter__ and __exit__ need to be implemented. The first one is called when entering a context (manager), the latter is called when leaving the context.

With this knowledge we can create a Timer class implementing both methods. When entering the context, we want to get the current time and save it to a start instance variable. If we leave the context, we want to get the current time and subtract the start time from it. The result is printed.

To customize the output, we let the user specify a description, which is printed before the elapsed time. The following gist shows you the ready-to-use class.


from time import time


class Timer(object):
    def __init__(self, description):
        self.description = description
    def __enter__(self):
        self.start = time()
    def __exit__(self, type, value, traceback):
        self.end = time()
        print(f"{self.description}: {self.end - self.start}")


with Timer("List Comprehension Example"):
    s = [x for x in range(10_000_000)]

Generator-based Context Manager

The generator-based approach is a bit more straightforward. Basically, we create a generator function containing the program flow (taking start and end time as well as printing the elapsed time). The @contextmanager decorator turns the generator function into a proper context manager by wrapping the generator by the GeneratorContextManager object.


from contextlib import contextmanager
from time import time


@contextmanager
def timing(description: str) -> None:
    start = time()
    yield
    ellapsed_time = time() - start

    print(f"{description}: {ellapsed_time}")


with timing("List Comprehension Example"):
    s = [x for x in range(10_000_000)]

Let’s describe it a bit less formal: What happens is, that entering the context (timing) results in taking the current time and playing the ball back to the code inside the context using yield . If the code inside of the with -block is executed, we jump back to the point right after the yield keyword. Now we calculate the elapsed time and print it. We are now leaving the context.

Summary

In this article you learned how to create your own timing context manager. After creating the basic concept, you implemented the context manager in two ways: Class-based and generator-based. The resulting class and generator function are ready-to-use.

I hope you enjoyed reading this article and learned something new. Feel free to share this article with your friends. Stay curious and keep coding!