Skip to content
aviral gupta

// A4.1 · ~30 min · Advanced

Measuring with timeit

After this lesson you can time code with timeit.timeit and timeit.repeat, report the minimum per call instead of one noisy figure, and compare two versions fairly: same input, same result, a ratio.

Lesson 1 of 5 in A4 Performance and numbers

Start of the module

You will be able to

  • Time a statement or a callable with timeit.timeit and timeit.repeat, using setup, number and globals
  • Explain why a single timing misleads and report the minimum of repeats per call
  • Compare two versions fairly: check they agree, time them the same way and judge the ratio
  1. Warm-up · Activity 1 of 7

    Warm-up from module A3: on the standard CPython build, with the GIL, which statement about threads is true?

  2. Predict · Activity 2 of 7

    Predict before you read on: work counts how often it is called. What does this print?

    import timeit
    
    calls = 0
    
    
    def work():
        global calls
        calls += 1
    
    
    timeit.timeit(work, number=1000)
    first = calls
    times = timeit.repeat(work, number=1000, repeat=5)
    print(first, calls, len(times))
  3. Practice · Activity 3 of 7

    Fill in the keyword so that the list is built once, outside the timed statement.

    import timeit
    
    setup_runs = []
    timeit.timeit("data.sort()", ____="data = [3, 1, 2]; setup_runs.append(1)", number=100, globals=globals())
    ="data = [3, 1, 2]; setup_runs.append(1)"
  4. Practice · Activity 4 of 7

    timeit.timeit(stmt) with the default number returned 0.84. How long does one run of stmt take?

  5. Practice · Activity 5 of 7

    Match each part of a timeit call to what it does.

  6. Brain teaser · Activity 6 of 7

    Brain teaser. The lambda records whether the garbage collector is on while it is being timed. What does this print?

    import gc
    import timeit
    
    flags = []
    timeit.timeit(lambda: flags.append(gc.isenabled()), number=3)
    print(flags, gc.isenabled())
  7. Apply · Activity 7 of 7

    Mini-task. Write two functions that build the text "0,1,2,…,999": one with ",".join, one with += in a loop. First print whether they give the same text. Then time each with timeit.repeat (number=200, repeat=5), and print the minimum per call in microseconds and the ratio. Run it three times: which figures change, and which conclusion stays?

    Check your work against this list

Build it yourself

Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.

Worked example

A fair timing of two versions

Two functions add up the numbers below n: one with a loop, one with a formula. The program first checks that they agree, then times each with timeit.repeat and keeps the fastest timing per call. The times themselves change on every run and on every machine, so the program prints only what does not: whether the results agree, and whether the ratio clears a threshold. Add a print of slow and fast to see the raw figures change.

main.py

import timeit
from collections.abc import Callable


def total_loop(n: int) -> int:
    total = 0
    for i in range(n):
        total += i
    return total


def total_formula(n: int) -> int:
    return n * (n - 1) // 2


def best_per_call(func: Callable[[], object], number: int, repeat: int = 5) -> float:
    """The fastest of `repeat` timings, divided by the calls in each timing."""
    times = timeit.repeat(func, number=number, repeat=repeat)
    return min(times) / number


n = 100_000
# 1. Same input, same answer: otherwise the timing compares different work.
print("same result:", total_loop(n) == total_formula(n))

# 2. Time both the same way. The seconds differ on every run.
slow = best_per_call(lambda: total_loop(n), number=20)
fast = best_per_call(lambda: total_formula(n), number=20)

# 3. Print what does not vary: the ratio against a threshold.
print("formula at least 100x faster:", slow / fast >= 100)
print("faster version:", "formula" if fast < slow else "loop")

Run it with

python main.py

Output

same result: True
formula at least 100x faster: True
faster version: formula
  • The correctness check comes first: timing two versions that disagree compares different work.
  • best_per_call divides by number, so the figures are per call whatever loop count you pick.
  • The ratio is thousands, so the threshold of 100 holds on any machine; the seconds do not.
  • Each lambda passes n in without timing any setup: the callable is what timeit calls.
Change it and run it

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.

Exercises

Exercise 1 of 2

The best time per call

Complete best_per_call(func, number=1000, repeat=5). It must call timeit.repeat (write import timeit and timeit.repeat, so the tests can replace it) with the function, number and repeat, and return the minimum of the timings divided by number. The tests count calls and hand in made-up timings, so they never depend on how fast this computer is.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.

Hints
  1. Hint 1

    timeit.repeat(func, number=number, repeat=repeat) returns a list with one total per timing.

  2. Hint 2

    Each total covers number calls, so the time per call is min(times) / number.

  3. Hint 3

    The whole body fits in two lines: times = timeit.repeat(...), then return min(times) / number.

Show a solution

One way to solve it. Yours can look different and still pass the checks.

import timeit
from collections.abc import Callable


def best_per_call(func: Callable[[], object], number: int = 1000, repeat: int = 5) -> float:
    times = timeit.repeat(func, number=number, repeat=repeat)
    return min(times) / number
Run it on your computer

Install Python 3.14 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.py

import timeit
from collections.abc import Callable


def best_per_call(func: Callable[[], object], number: int = 1000, repeat: int = 5) -> float:
    # One timing is not enough: take `repeat` timings of `number` calls each,
    # and return the fastest one divided by `number`.
    return timeit.timeit(func, number=number)

test_main.py

import math
import timeit
from unittest import mock

from main import best_per_call


def test_calls():
    """number=10 and repeat=3 call the function 30 times"""
    calls = []
    best_per_call(lambda: calls.append(1), number=10, repeat=3)
    assert len(calls) == 30, f"the function was called {len(calls)} times, expected 30 (10 calls in each of 3 timings)"


def test_minimum_per_call():
    """Timings of 0.5, 0.2 and 0.4 s for 100 calls give 0.002 s per call"""
    with mock.patch("timeit.repeat", return_value=[0.5, 0.2, 0.4]) as fake:
        got = best_per_call(lambda: None, number=100, repeat=3)
    assert fake.called, "best_per_call did not call timeit.repeat"
    assert math.isclose(got, 0.002), f"got {got!r}, expected 0.002: the minimum, 0.2, divided by number=100"


def test_defaults():
    """Without arguments: 5 timings of 1000 calls"""
    calls = []
    best_per_call(lambda: calls.append(1))
    assert len(calls) == 5000, f"the function was called {len(calls)} times, expected 5000"


def test_returns_float():
    """A real run returns a positive float"""
    got = best_per_call(lambda: sum(range(10)), number=50)
    assert isinstance(got, float) and got > 0, f"got {got!r}, expected a positive float"

On macOS and Linux, type python3 wherever these commands say python, as in the first lesson.

Run the program:

python main.py

Run the checks (needs learnrun.py in the same folder):

python learnrun.py test
Download learnrun.py

Exercise 2 of 2

A fair comparison

Complete compare(slow, fast, arg, number=200). First check that slow(arg) and fast(arg) are equal, and raise ValueError if not, before timing anything. Then time lambda: slow(arg) and lambda: fast(arg) with timeit.repeat (number=number, repeat=5) and return min of the slow timings divided by min of the fast timings. A result of 3.0 means the fast version is three times faster.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.

Hints
  1. Hint 1

    Start with if slow(arg) != fast(arg): raise ValueError(...). Only then time anything.

  2. Hint 2

    timeit.repeat(lambda: slow(arg), number=number, repeat=5) gives five totals; keep min() of them.

  3. Hint 3

    The ratio is slow over fast: a bigger number means a bigger win for the fast version.

Show a solution

One way to solve it. Yours can look different and still pass the checks.

import timeit
from collections.abc import Callable


def compare(slow: Callable[[int], object], fast: Callable[[int], object], arg: int, number: int = 200) -> float:
    if slow(arg) != fast(arg):
        raise ValueError("the two versions give different results")
    slow_best = min(timeit.repeat(lambda: slow(arg), number=number, repeat=5))
    fast_best = min(timeit.repeat(lambda: fast(arg), number=number, repeat=5))
    return slow_best / fast_best
Run it on your computer

Install Python 3.14 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.py

import timeit
from collections.abc import Callable


def compare(slow: Callable[[int], object], fast: Callable[[int], object], arg: int, number: int = 200) -> float:
    # 1. Raise ValueError if slow(arg) and fast(arg) differ.
    # 2. Time both with timeit.repeat and return min(slow) / min(fast).
    slow_time = timeit.timeit(lambda: slow(arg), number=number)
    fast_time = timeit.timeit(lambda: fast(arg), number=number)
    return fast_time / slow_time

test_main.py

import math
from unittest import mock

from main import compare

who = []


def slow(n):
    who.append("slow")
    return n * 2


def fast(n):
    who.append("fast")
    return n + n


def wrong(n):
    who.append("wrong")
    return n


def fake_repeat(stmt, number=1000000, repeat=5, **kwargs):
    """Calls stmt once, then returns made-up timings for whichever version ran."""
    stmt()
    return [0.9, 0.6, 0.8] if who[-1] == "slow" else [0.3, 0.2, 0.25]


def test_ratio():
    """Best times of 0.6 s and 0.2 s give a ratio of 3.0"""
    with mock.patch("timeit.repeat", side_effect=fake_repeat):
        got = compare(slow, fast, 21)
    assert math.isclose(got, 3.0), f"got {got!r}, expected 3.0: min of the slow timings / min of the fast timings"


def test_different_results():
    """Versions with different results raise ValueError"""
    try:
        compare(slow, wrong, 21)
    except ValueError:
        return
    raise AssertionError("compare(slow, wrong, 21) did not raise ValueError, although 42 != 21")


def test_checks_before_timing():
    """Different results are caught before anything is timed"""
    with mock.patch("timeit.repeat", side_effect=fake_repeat) as fake:
        try:
            compare(slow, wrong, 21)
        except ValueError:
            pass
    assert fake.call_count == 0, f"timeit.repeat was called {fake.call_count} times before the check: compare the results first"


def test_real_run():
    """A real run returns a positive float"""
    got = compare(slow, fast, 5, number=20)
    assert isinstance(got, float) and got > 0, f"got {got!r}, expected a positive float"

On macOS and Linux, type python3 wherever these commands say python, as in the first lesson.

Run the program:

python main.py

Run the checks (needs learnrun.py in the same folder):

python learnrun.py test
Download learnrun.py

Common mistakes

A string statement cannot see your function

import timeit


def square(n):
    return n * n


print(timeit.timeit("square(3)", number=10))

What Python prints

NameError: name 'square' is not defined

Why, and the fix

A string statement runs in timeit’s own namespace, which does not contain your functions. Pass globals=globals(), import it in setup ("from __main__ import square"), or pass a callable: timeit.timeit(lambda: square(3), number=10).

Passing the result instead of the function

import timeit


def work():
    sum(range(100))


print(timeit.timeit(work(), number=10))

What Python prints

ValueError: stmt is neither a string nor callable

Why, and the fix

work() calls the function once, before timeit starts, and passes its result, None. Pass the function itself, timeit.timeit(work, number=10), or a lambda when it needs arguments.

Treating the list from repeat as one number

import timeit

times = timeit.repeat("sum(range(100))", number=1000, repeat=5)
print(times / 1000)

What Python prints

TypeError: unsupported operand type(s) for /: 'list' and 'int'

Why, and the fix

repeat returns a list with one total per timing. Pick one figure first, the minimum, then divide by number: min(times) / 1000 is the best time per call.

Python in the browser: Pyodide 314.0.7, MPL-2.0. Licence and source

Exit ticket

5 questions, no hints. Score 80% or more to complete the lesson.

Finish every activity above to unlock the exit ticket.

Report a problem

Spotted something wrong or unclear? Say what, and it will be checked and fixed.

#

At least 20 characters.

Only if you want a reply.

Key ideas

timeit runs your code many times

timeit.timeit(stmt, setup, number=1_000_000, globals=None) runs setup once, then runs stmt number times and returns the total in seconds, as a float. stmt and setup can be strings or callables with no arguments. A string runs in timeit’s own namespace, so pass globals=globals() to see your functions. Divide the total by number to get the time per call: 0.84 s for a million calls is 0.84 µs each. The command line, python -m timeit, picks number for you; the function does not.

Why a single timing lies

Other processes, the operating system and caches all slow some runs down, and never speed one up beyond what the code can do. timeit.repeat(stmt, number=…, repeat=5) makes several timings and returns them as a list. The documentation advises the minimum: higher values come from interference, not from Python being slower. Report min(times) / number. timeit also switches the garbage collector off while it times, so code that creates many objects may run slower in a real program.

Compare fairly, and never test a time

Before timing two versions, check that they return the same result for the same input: a fast wrong answer is worthless. Time both with the same number and repeat, keep data building in setup, and judge the ratio, such as "at least 10x faster". Absolute times differ per machine and per run, so a test must never assert one. Tests can count calls or check results; a ratio with a wide margin is the most a timing should promise.

Sources

Last reviewed September 29, 2026