Warm-up · Activity 1 of 7
// 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
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
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))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)"Practice · Activity 4 of 7
timeit.timeit(stmt) with the default number returned 0.84. How long does one run of stmt take?
Practice · Activity 5 of 7
Match each part of a timeit call to what it does.
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())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.pyOutput
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
Hint 1
timeit.repeat(func, number=number, repeat=repeat) returns a list with one total per timing.
Hint 2
Each total covers number calls, so the time per call is min(times) / number.
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.pyRun the checks (needs learnrun.py in the same folder):
python learnrun.py testDownload learnrun.pyExercise 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
Hint 1
Start with if slow(arg) != fast(arg): raise ValueError(...). Only then time anything.
Hint 2
timeit.repeat(lambda: slow(arg), number=number, repeat=5) gives five totals; keep min() of them.
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.pyRun the checks (needs learnrun.py in the same folder):
python learnrun.py testDownload learnrun.pyCommon 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 definedWhy, 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 callableWhy, 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.