Here’s the situation, you have two algorithms in front of you, and you want to see which one is faster. Suppose we wanted to see what’s faster for constructing a list: a for loop, or a list comprehension. A simple solution might look like this.

import time
def number_list_1(count_up_to):
    lst=[]
    for i in range(count_up_to):
        lst.append(i)

def number_list_2(count_up_to):
    lst=[i for i in range(count_up_to)]

if __name__=="__main__":
    import time

    start = time.time()
    number_list_1(10000)
    end =time.time()
    print(end-start)

    start = time.time()
    number_list_2(10000)
    end = time.time()
    print(end-start)

Output

0.0009999275207519531
0.0010006427764892578

This technique yields wildly inconsistent results. According to these results using a for loop is much faster but that’s not true, list comprehensions are highly optimized, and I will prove this using timeit.

Basics of timeit.

timeit has two interfaces, a command line interface, and a python interface. I personally have never used the command line interface. But here’s an example of the command line interface. An important point to notice is that the Python code is passed as a string.

Command line interface

$ python -m timeit "[i for i in range(100)]"
100000 loops, best of 5: 3.74 usec per loop

Python interface

This is the interface that I primarily tend to use and most likely you will too. The interface defined by timeit docs is

timeit.timeit(stmt='pass',setup='pass',timer=<default timer>,number=1000000,globals=None)

For now let’s focus on three parameters stmt, setup, and number. number Is the number of times the statement/code snippet is run, setup takes in the code required for the statement to run, this includes things like function definitions, import statements, etc. stmt takes in the code snippet to be run. Let’s see this in action.

# previously defined function
def number_list_1(count_up_to):
    lst=[]
    for i in range(count_up_to):
        lst.append(i)

# put the function in a multi line string 
setup_one="""
def number_list_1(count_up_to):
    lst=[]
    for i in range(count_up_to):
        lst.append(i)
"""

# Put the function call inside a string
stmt_one = "number_list_1(100)"

if __name__=="__main__":
    import timeit  #import 
    print(timeit.timeit(stmt=stmt_one, setup=setup_one)) # call timeit and pass arguments

It is quite inconvennient to put the function definition inside a string, so there is a slightly better way to do this. In the setup, we can use an important statement like this.

# previously defined function
def number_list_1(count_up_to):
    lst=[]
    for i in range(count_up_to):
        lst.append(i)
if __name__=="__main__":
    import timeit  
    print(timeit.timeit(stmt="number_list_1(100)", 
        setup="from __main__ import number_list_1")) #new

Comparing for loops and list comprehensions.

Here is where timeit really shines.

def number_list_1(count_up_to):
    lst=[]
    for i in range(count_up_to):
        lst.append(i)

def number_list_2(count_up_to):
    lst=[i for i in range(count_up_to)]

if __name__=="__main__":
    import timeit  
    print(timeit.timeit(stmt="number_list_1(100)", 
        setup="from __main__ import number_list_1"))
    print(timeit.timeit(stmt="number_list_2(100)", 
        setup="from __main__ import number_list_2"))

Output-

4.5117294
2.2363461000000004

Notice how we got a different result. List comprehensions are indeed faster, in this case almost twice as fast.

pretty-timeit: A prettier alternative to timeit.

All the timeit is an excellent module, I’ve always had difficulty using and remembering the syntax, so I wrote a tiny Python package called pretty-timeit that provides the same functionality of timeit but with an easier, more intuitive interface. You can check out the project on github

Let’s run the same example, pretty-timeit. First we must download the package.

$ pip install pretty-timeit

pretty-timeit uses Python’s decorator syntax. Now open the same file we used.

from ptimeit import timethis, Timer # new

@timethis([100]) # new 
def number_list_1(count_up_to):
    lst=[]
    for i in range(count_up_to):
        lst.append(i)

@timethis([100]) # new
def number_list_2(count_up_to):
    lst=[i for i in range(count_up_to)]

if __name__=="__main__":
    Timer.run(compare=True) # new

Run the script, and you should see an output like this.

rank | name          | Execution time
1    | number_list_2 | 2.4198003000000003
2    | number_list_1 | 4.9047559000000005

The compare=True Flag just formats the output, and orders the functions from fastest to slowest. output without the compare flag would look like this.

name          | Execution time
number_list_1 | 4.4723315
number_list_2 | 2.2453294

Before I finish I’m going to show off one more feature of pretty-timeit. Our functions are poorly named, and I would not recommend using this naming convention in your code. Let’s say, you couldn’t change the name of the function but you wanted the output to show a custom description of the function, This is how you can do it in pretty-timeit

from ptimeit import timethis, Timer

@timethis([100], name="Using for loops" ) # new
def number_list_1(count_up_to):
    lst=[]
    for i in range(count_up_to):
        lst.append(i)

@timethis([100], name="Using list comprehensions") # new
def number_list_2(count_up_to):
    lst=[i for i in range(count_up_to)]

if __name__=="__main__":
    Timer.run(compare=True)
output:
rank | name                      | Execution time
1    | Using list comprehensions | 2.2104532999999993
2    | Using for loops           | 4.3632882

Isn’t that more convenient? If you’re interested in pretty-timeit, You can read the detailed documentation on github. If you find something confusing, or missing, or a typo, You can open up an issue or make a pull request. I’d be very happy to help.