mattalcock.com

· Python · From the archive

Timing Python code with a timeit decorator

A simple Python timeit decorator to time functions without cluttering your code, plus a modern Python 3 version using perf_counter and functools.wraps.

First published on blog.mattalcock.com in February 2013, when I was leading data analytics at NaturalMotion. The original text and code are kept as written, with spelling fixed. What's changed since is in the 2026 note at the end.

This post outlines why timing code in Python is important and provides some simple decorators that can help you time your code without the concerns and worries of peppering your lovely clean code with temporary timing and print statements.

Scroll down if you're just after the decorator code to time functions....

Python vs Speed

One of things Python was originally criticised for was speed. Like lots of Dynamic Languages there is an overhead in keeping tracking of types and because code is interpreted at runtime instead of being compiled to native code at compile time dynamic languages like Python will always be a little slower.

Where Python shines is in its power and ability to allow programmers to optimise and focus on the algorithm. Focusing on the complexity of the problem and the algorithms order of magnitude rather than the low level details of memory management, pointers etc can often have massive benefits. Ask any computer science student and they can list off numerous teachings that show algorithm and data structure design will beat brute force computation power.

If you're looking to build something where microseconds count then I'd turn to C or Java. PyPy and other sophisticated JIT (Just in Time) compilers can help and they seem to be the future for Python solutions in this space. Another alternative is to find the slow code and either optimise that function or write a C plugin for Python for your very specific task. This last approach seems very popular in the finance industry where milliseconds mean dollars but they still need the flexibility and speed of development benefits that come with a dynamic language.

More often than not slow code just needs some refactoring work, a new support data structure or a change in the complexity of processing. So the challenge is really not how can I speed up my code but what code needs my attention.

Finding Slow Code

In order to find slow Python code we're going to have to time stuff. We don't want to cover our lovely clean code with temporary timing code and print statements, so how can we:

  • Time code without altering the code of a function
  • Get detailed timing information if the function is run with different arguments
  • Switch off the timing at deploy time to reduce the overhead and improve the performance of monitoring

The timing decorators below can help with all of these. If you're new to decorators and annotations see my previous blog post on the subject

The Timeit Decorator

import time

def timeit(f):

    def timed(*args, **kw):
        ts = time.time()
        result = f(*args, **kw)
        te = time.time()

        print 'func:%r args:[%r, %r] took: %2.4f sec' % \
          (f.__name__, args, kw, te-ts)
        return result

    return timed

Using the decorator is easy either use annotations.

@timeit
def compute_magic(n):
    #function definition
    ....

Or realias the function you want to time.

compute_magic = timeit(compute_magic)

Sometimes you'll want to remove the code timing. You can either do this by removing the timeit annotations before deployment or you can use a configuration switch to enable the decorator to wrap the function in timing code.

import time

#from config import TIME_FUNCTIONS
TIME_FUNCTIONS = False

def timeit(f):
    if not TIME_FUNCTIONS:
        return f
    else:
        def timed(*args, **kw):
            ts = time.time()
            result = f(*args, **kw)
            te = time.time()

            print 'func:%r args:[%r, %r] took: %2.4f sec' % \
                (f.__name__, args, kw, te-ts)
            return result

        return timed

By simply changing the TIME_FUNCTIONS configuration switch the functions will not be decorated. I find having these switches in a common config file/folder often helps.

All this code and the majority of code from my posts can be found in the hacks repo of my github account. Please take a look here. I hope it's helped if there are any questions about the above or you'd like to understand more about timing code in Python drop me a mail.

2026What I'd change now

The decorator above is Python 2. Here's how I'd write it in Python 3:

import functools
import time

def timeit(func):
    @functools.wraps(func)
    def timed(*args, **kwargs):
        start = time.perf_counter()
        try:
            return func(*args, **kwargs)
        finally:
            elapsed = time.perf_counter() - start
            print(f"{func.__name__} took {elapsed:.4f}s")
    return timed

@timeit
def compute_magic(n):
    return sum(i * i for i in range(n))
  • time.perf_counter() is the right clock for measuring durations: high resolution, and it never jumps when the system clock changes, unlike time.time().
  • functools.wraps keeps the original function's name and docstring, so logs, tracebacks and tools still see compute_magic, not timed.
  • try/finally means you still get a timing when the function raises.

A decorator is the right tool for timing real calls in a running program. For comparing two small snippets, use the standard library's timeit module, which repeats the code many times to smooth out noise: python -m timeit "sum(i * i for i in range(1000))". And to find which code is slow in the first place, start with the profiler: python -m cProfile -s cumtime your_script.py.

← All writingOriginal source on GitHub