Measure Python Performance with `timeit`

Learn how to benchmark Python code accurately with the timeit module, compare different approaches, and avoid common timing pitfalls.

8 min read

The Python timeit module measures how long a small piece of code takes to run. It handles the details that make manual timing unreliable: it runs the code many times to smooth out noise, disables garbage collection so allocation patterns do not skew the result, and picks a sensible number of repetitions automatically.

Use timeit when you have two different ways to write the same logic and you want to know which one is faster. It answers narrow, concrete questions like "is a list comprehension faster than a for loop for building a list?" or "which string formatting method is quickest?"

timeit is not the right tool for profiling a whole program. For that, use cProfile.

timeit is for micro-benchmarks: measuring a single expression or a few lines of code in isolation. The broader topic of Python performance optimization puts timeit into context alongside profiling and memory analysis.

How timeit works

When you call timeit.timeit(), the module compiles your code into a form it can run in a loop, disables garbage collection, runs the code repeatedly, and returns the total elapsed time. It chooses the number of repetitions so the total measurement lasts at least 0.2 seconds.

pythonpython
import timeit
 
elapsed = timeit.timeit(
    "sum(range(100))",
    number=10_000,
)
print(f"10,000 runs: {elapsed:.4f} seconds")

The output is a single number: the total time for ten thousand runs. Divide by 10,000 to get the per-call cost.

If you do not pass the number parameter, timeit picks a value automatically. The chosen count is available as a return value, but the simpler approach is to pass number explicitly when you want a specific count.

timeit measures the code as an isolated unit. It does not measure imports, variable setup, or surrounding context unless you include those in the snippet or in the setup parameter.

Using the setup parameter

Most real benchmarks need imports, functions, or data before the code under test can run. The setup parameter runs once before the timed repetitions begin. It does not count toward the measured time.

pythonpython
import timeit
 
setup_code = """
data = list(range(10_000))
"""
 
test_code = """
squares = [x * x for x in data]
"""
 
elapsed = timeit.timeit(
    test_code,
    setup=setup_code,
    number=1_000,
)
print(f"1,000 runs: {elapsed:.4f}s")

The setup block creates a list of ten thousand numbers exactly once. Then timeit runs the list comprehension one thousand times and reports the total. Without it, the list creation would run inside the timed loop and inflate the result.

A common mistake is putting imports inside the timed snippet instead of in the setup. Importing a module takes time, and you usually want to measure your code, not the import system.

pythonpython
# Wrong: import is timed every repetition
bad = timeit.timeit("import math; math.sqrt(25)")
 
# Right: import happens once in setup
good = timeit.timeit(
    "math.sqrt(25)",
    setup="import math",
)

The timing difference can be meaningful. The wrong version measures import overhead multiplied by the repetition count, which can dominate the result and hide the performance of the code you actually care about.

Getting more reliable results with repeat

A single timeit.timeit() call runs the code many times but reports one total. That total can be affected by a brief CPU spike, a background process, or thermal throttling on the machine.

timeit.repeat() runs the entire timing experiment multiple times independently and returns a list of results. The minimum value from that list is usually the most trustworthy figure because it represents the run with the least system interference.

pythonpython
import timeit
 
setup = "data = list(range(10_000))"
stmt = "[x * x for x in data]"
 
results = timeit.repeat(
    stmt,
    setup=setup,
    number=1_000,
    repeat=5,
)
print(f"All results: {[f'{r:.4f}' for r in results]}")
print(f"Best (min): {min(results):.4f}s")

Running the experiment five times produces five results. The minimum is the best representation of how fast the code can run on the current hardware under ideal conditions.

The repeat parameter controls how many independent timing runs to perform. Five is a common default in the command-line interface.

For casual comparisons, three is enough. For publication-quality benchmarks, use five or seven.

The command-line interface

For quick experiments, python -m timeit is faster than writing a script. It accepts the statement as a command-line argument and reports the best result from multiple repetitions.

texttext
$ python -m timeit "'-'.join(str(n) for n in range(100))"
20000 loops, best of 5: 15.2 usec per loop
 
$ python -m timeit "'-'.join([str(n) for n in range(100)])"
50000 loops, best of 5: 10.1 usec per loop

The output shows the number of loops run per repetition, the number of repetitions, and the best per-loop time in microseconds. The second version is faster because str.join needs to build an internal sequence anyway, and a list comprehension builds that sequence more efficiently than a generator expression.

For multi-line snippets, the -s flag sets up the environment, and you can pass the statement as a string:

texttext
$ python -m timeit -s "nums = list(range(1000))" "sum(nums)"

The command-line interface is ideal for fast sanity checks. If you are not sure whether one approach is faster than another, a 10-second python -m timeit test often answers the question before you commit to a larger change.

Avoiding common timing mistakes

Mistake 1: measuring I/O-bound code

timeit measures CPU time, not wall-clock time with I/O. Running a network request or file read inside a timeit snippet repeats the I/O thousands of times, which is slow, unreliable, and not what the tool is designed for.

pythonpython
# Do not do this. timeit will repeat the HTTP call thousands of times.
bad = timeit.timeit("requests.get('https://example.com')", setup="import requests")

Use timeit for pure computation: arithmetic, string operations, list building, sorting, filtering, and algorithm comparisons. For I/O-heavy code, measure at the program level with a profiler or manual wall-clock timing.

Mistake 2: not using setup for data creation

If the code under test needs a large dataset and the dataset creation is inside the timed snippet, the measurement includes data creation cost. That may be what you want in some cases, but most benchmarks care about the operation, not the setup.

pythonpython
# Problematic: data creation inside the timed snippet
stmt_bad = """
data = list(range(100_000))
result = max(data)
"""
 
# Better: data creation in setup, only max() is timed
setup = "data = list(range(100_000))"
stmt_good = "max(data)"

Mistake 3: running too few repetitions

If timeit runs the code only a few times, the measurement can be dominated by noise. Trust the module's automatic repetition count or pass a number large enough that the total run lasts at least 0.1 seconds.

Mistake 4: comparing results across different machines

A benchmark result is only valid on the machine where it was measured. CPU model, clock speed, memory latency, operating system version, and Python version all affect timing. Share benchmarks as relative comparisons, not absolute numbers.

Mistake 5: measuring code the optimizer can eliminate

If the result of the timed expression is never used, Python's peephole optimizer or a just-in-time compiler might remove the computation entirely. Always ensure the measured code has a visible side effect or assign the result to a variable that is used outside the timed block.

pythonpython
# Risky: the optimizer might skip the computation
stmt = "sum(range(1_000_000))"
 
# Safer: assign the result so it cannot be optimized away
stmt = "total = sum(range(1_000_000))"

In practice, CPython's optimizer is conservative and rarely eliminates meaningful computation, but the habit of capturing results is still good practice for correctness.

Comparing algorithms with timeit

A practical use of timeit is comparing different algorithms for the same task. Suppose you want to know whether checking membership in a list or a set is faster.

pythonpython
import timeit
 
setup = """
items_list = list(range(10_000))
items_set  = set(range(10_000))
"""
 
list_time = timeit.timeit("5_000 in items_list", setup=setup, number=10_000)
set_time  = timeit.timeit("5_000 in items_set",  setup=setup, number=10_000)
 
print(f"List membership: {list_time:.4f}s")
print(f"Set membership:  {set_time:.4f}s")

For ten thousand items, the list version scans up to half the list on average. The set version computes a hash and does one lookup. The timing difference confirms what the theory predicts: O(n) versus O(1).

These comparisons are most useful when the faster approach is not obvious. For example, comparing list.sort() with sorted(list) or comparing different string formatting methods. The answer sometimes depends on Python version, so measuring is always better than assuming.

Timing function calls

timeit can also measure how long a function call takes versus an inline expression. The Timer class gives you more control over the measurement when you need it.

pythonpython
import timeit
 
def compute():
    return sum(i * i for i in range(10_000))
 
timer = timeit.Timer("compute()", globals=globals())
elapsed = timer.timeit(number=1_000)
print(f"Function: {elapsed:.4f}s")

Passing globals=globals() lets timeit see the compute function defined in the current scope. Without it, the snippet runs in an empty namespace and the function is not found.

Putting timeit into your workflow

Make timeit a habit when comparing two approaches. The workflow is simple:

  1. Write both versions of the code.

  2. Wrap each version in a timeit call with the same setup and the same number.

  3. Run both timings.

  4. Use the faster version unless the slower version is significantly more readable and the performance difference does not matter.

A concrete example with string building:

pythonpython
import timeit
 
setup = "words = ['hello', 'world', 'python', 'code'] * 250"
 
concat_time = timeit.timeit(
    "result = ''; [result := result + w for w in words]",
    setup=setup,
    number=500,
)
 
join_time = timeit.timeit(
    "result = ' '.join(words)",
    setup=setup,
    number=500,
)
 
print(f"String concatenation: {concat_time:.4f}s")
print(f"str.join:             {join_time:.4f}s")

The str.join approach is consistently faster because it allocates the result string once. Repeated concatenation allocates a new string at every step, and the cost grows with string length. timeit makes this gap visible so you can choose the right approach with confidence.

The broader topic of Python performance optimization puts timeit into context alongside profiling, memory analysis, and algorithmic improvements. For whole-program analysis rather than micro-benchmarks, the next article on profiling with cProfile covers the tool that tells you which functions dominate your program's runtime.

Rune AI

Rune AI

Key Insights

  • timeit runs code many times and reports reliable timing by disabling GC and picking an appropriate repetition count automatically.
  • Always use the setup parameter for imports and variable definitions needed by the snippet under test.
  • Use timeit.repeat() and take the minimum value when comparing approaches to filter out system noise.
  • The command-line interface python -m timeit is often the fastest way to test a quick idea.
RunePowered by Rune AI

Frequently Asked Questions

What is the Python timeit module?

timeit is a standard library module that measures execution time of small code snippets. It runs code multiple times, disables garbage collection during measurement, and picks an appropriate repetition count automatically for reliable results.

Should I use timeit.timeit() or timeit.repeat()?

Use timeit.timeit() for a quick single measurement. Use timeit.repeat() when you want multiple independent measurements to assess variability. The minimum of those runs is the most reliable figure because it represents best-case performance with minimal system interference.

Conclusion

The timeit module is the right tool for benchmarking small Python code snippets. It handles repetition, disables garbage collection, and produces reliable measurements when used correctly. Always include a setup step when your snippet depends on imports or variables, use repeat with the minimum value when comparing approaches, and never trust a single manual timing when timeit can do the job more rigorously.