Python timeit: Reliable Microbenchmarks with repeat()

Quick answer: Use timeit for a focused microbenchmark: put setup outside the timed statement, run enough iterations, repeat the measurement, and inspect the result vector. timeit reduces common timing traps, but it does not explain an entire application’s call graph or replace production profiling.

Python Pool infographic showing timeit setup statement repeated runs autorange best result and benchmark interpretation
Use timeit for narrow, repeatable microbenchmarks; isolate setup, run multiple repetitions, inspect the full result vector, and avoid treating one number as application profiling.

timeit is Python’s standard tool for measuring small code snippets. It runs the snippet many times, reduces setup noise, and reports elapsed time so you can compare approaches more fairly than with one manual timestamp.

Use it for focused microbenchmarks: a small expression, a function call, a parsing step, or a data-structure operation. It is not a full performance profiler for an entire application, but it is excellent for answering narrow questions.

The official Python timeit documentation explains timeit(), repeat(), Timer, setup code, and command-line usage. The official time module documentation covers clocks such as perf_counter().

Measure A Small Statement

The simplest form passes a statement string and the number of executions.

import timeit

elapsed = timeit.timeit("sum(range(100))", number=10_000)

print(elapsed)
print(elapsed / 10_000)

The first printed value is total elapsed seconds. Dividing by the loop count gives an average per execution, which is often easier to compare.

Use enough executions to make the result stable, but avoid numbers so large that each experiment becomes slow to run.

Provide Setup Code

Use the setup argument when the timed statement needs imports or one-time preparation.

import timeit

elapsed = timeit.timeit(
    "math.sqrt(144)",
    setup="import math",
    number=100_000,
)

print(elapsed)

The setup code is not included in the measured loop. That keeps the result focused on the operation you are comparing.

Do not hide expensive work in setup if that work happens every time in the real program. The benchmark should match the real question you are asking.

Python Pool infographic showing code snippet, timeit repeat, runs, and timing samples
repeat produces multiple timing samples so noisy single measurements are easier to assess.

Compare Two Functions

For functions already defined in your file, pass globals=globals() so timeit can find them.

import timeit

def use_join(parts):
    return "-".join(parts)

def use_loop(parts):
    text = ""
    for part in parts:
        text += part + "-"
    return text.rstrip("-")

parts = ["a", "b", "c", "d"]

join_time = timeit.timeit("use_join(parts)", globals=globals(), number=50_000)
loop_time = timeit.timeit("use_loop(parts)", globals=globals(), number=50_000)

print(join_time)
print(loop_time)

This pattern is clearer than placing long function definitions inside strings. It also lets editors and syntax checkers inspect the code normally.

Run both snippets with the same input and loop count. Otherwise, the comparison is not meaningful.

Repeat Runs And Choose The Best

Timing results vary because of CPU scheduling, background tasks, caching, and other system activity. repeat() runs the benchmark several times.

import timeit

runs = timeit.repeat(
    "sorted([3, 1, 2, 5, 4])",
    repeat=5,
    number=20_000,
)

print(runs)
print(min(runs))

The documentation recommends looking at the minimum run for microbenchmarks because higher runs are often affected by outside interference.

Still, large spread between repeats is a warning sign. It may mean the benchmark is too small, the machine is busy, or the operation depends on external state.

Python Pool infographic comparing setup code, statement, timer, and measured operation
Keep setup outside the measured statement when comparing operation costs.

Let Timer Pick A Loop Count

Timer.autorange() finds a loop count that gives a measurable runtime.

import timeit

timer = timeit.Timer("value * value", setup="value = 12")

loops, seconds = timer.autorange()

print(loops)
print(seconds)
print(seconds / loops)

This is useful when you do not know a good number value ahead of time. Fast operations need many loops, while slower operations need fewer.

After autorange gives a reasonable loop count, you can repeat the benchmark with that count to compare several approaches consistently.

Use perf_counter For Larger Blocks

For whole workflows, use time.perf_counter() around the code block. That measures elapsed wall-clock time without forcing the code into a small string.

from time import perf_counter

start = perf_counter()

total = sum(i * i for i in range(100_000))

elapsed = perf_counter() - start

print(total)
print(elapsed)

perf_counter() is better for coarse timing. timeit is better for small repeated snippets where setup, loop count, and repeat control matter.

Benchmark Carefully

Good timing tests start with a specific question. Instead of asking whether one program is faster than another, ask whether one small operation is faster for a representative input size. Narrow questions produce results you can act on.

Keep setup separate from the timed statement, but only when setup is not part of the real cost. For example, building a list once and sorting it many times may answer a different question than building and sorting the list every time.

Use realistic data sizes. Some approaches are faster for ten items and slower for a million items. If the real workload is large, test more than one size before choosing an implementation.

Run benchmarks on a quiet machine when possible. Browser tabs, indexing, virtual machines, and background sync tools can add noise. Repeating runs and checking the minimum helps, but it does not remove every source of interference.

Record the Python version, platform, input size, and code being measured when a timing result informs an engineering decision. Without that context, a number from timeit is hard to reproduce later.

Do not treat a microbenchmark as the final answer for application speed. Inputs, data size, memory behavior, and surrounding code can change the outcome. Use timeit to guide choices, then measure the real workload before making a broad performance claim.

Python Pool infographic comparing noisy samples, minimum time, repeat values, and result
The minimum sample often approximates an uncontended lower bound, not a universal runtime.

Time A Callable With A Shared Namespace

Passing a callable is convenient for a small function. For string statements, pass globals=globals() or an explicit namespace so the timed code can access the objects it needs without repeating imports in every run.

import timeit


def build_values():
    return [str(number) for number in range(100)]

seconds = timeit.timeit(build_values, number=1000)
print(seconds)

Keep Setup Out Of The Measurement

Use setup for imports or data preparation that should happen once per timing run. If setup is included in the statement, the result answers a different question and may hide the operation you intended to compare.

import timeit

seconds = timeit.timeit(
    stmt="'-'.join(map(str, values))",
    setup="values = range(100)",
    number=1000,
)
print(seconds)
Python Pool infographic testing warmup, input size, environment, timers, and validation
Control input size, environment, timer choice, setup, and statistical interpretation.

Repeat And Inspect The Results

repeat() returns several elapsed values. The minimum is often a useful lower-bound signal, but inspect all values because system scheduling and background work can affect slower repetitions.

import timeit

results = timeit.repeat("sum(range(100))", repeat=5, number=1000)
print(results)
print(min(results))

Use autorange For A Small Statement

autorange() increases the loop count until the timing run is long enough to be measurable. It is useful for tiny operations, but record the Python version, hardware, input shape, and benchmark code when comparing results across machines.

import timeit

timer = timeit.Timer("sum(range(100))")
number, elapsed = timer.autorange()
print(number, elapsed)

The official timeit documentation covers setup, repeat(), autorange(), callable statements, globals, and result interpretation. For larger blocks, compare the related Python timer guide and use a profiler when the question is broader than one operation.

For related performance measurement, compare the Python timer tools, trace module, and threading vs multiprocessing when deciding whether a microbenchmark or broader diagnostic is needed.

Frequently Asked Questions

What is Python timeit used for?

timeit measures small code snippets repeatedly so you can compare focused operations with less timing noise than one manual timestamp.

What is the difference between timeit() and repeat()?

timeit() runs a statement a specified number of times, while repeat() runs that timing several times and returns a list of elapsed results.

Why is the minimum time often reported?

The fastest repetition is a useful lower-bound signal because slower runs often include interference from other processes, but the full result vector still needs context.

Is timeit a replacement for a profiler?

No. Use timeit for narrow microbenchmarks and a profiler or production measurement for understanding an entire application’s call graph and workload.

Subscribe
Notify of
guest
0 Comments
Oldest
Newest Most Voted