šŸš€ LEVEL UP TO SENIOR:Unlock 500+ Advanced Practical Challenges & Exercises.
šŸŽ“ COURSERA PARTNER:Earn professional Google, Meta, and IBM certificates to supercharge your resume.
HTML MASTER CLASS /// LEARN TAGS /// BUILD STRUCTURE /// SEMANTIC WEB /// HTML MASTER CLASS /// LEARN TAGS ///

Python Benchmarking

timeit and the statistical discipline needed to fairly compare two implementations — because a single time.time() measurement is nearly always misleading.

⚔ Total XP: 0|šŸ’» python XP: 0

Skill Matrix

UNLOCK NODES BY LEARNING NEW TAGS.

System Hub

Core logic.

Quick Quiz //

Why does a single time.time() measurement typically give a different number on each run of the exact same code?


šŸš€ LEVEL UP TO SENIOR:Unlock 500+ Advanced Practical Challenges & Exercises.
šŸŽ“ COURSERA PARTNER:Earn professional Google, Meta, and IBM certificates to supercharge your resume.

Comparing two implementations' speed sounds simple: run both, see which is faster. In practice, a single measurement is noise-dominated and frequently misleading. This lesson covers timeit and the statistical rigor needed to draw a real conclusion instead of an artifact of measurement noise.

1Why a Single Measurement Is Nearly Always Misleading

Wrapping code between two time.time() calls feels like measuring performance, and in a narrow, technically-true sense it is — but a single measurement captures not just your code's execution time, but also whatever OS scheduling happened to occur during that exact window, whatever CPU cache state happened to exist, and whatever else was running on the machine competing for resources at that moment. Run the identical code five times and you'll typically see five different numbers, sometimes varying by a meaningful percentage — and from a single run, there's no way to know whether a given number reflects the code's true typical speed or an unusually fast or slow moment.

This matters enormously when comparing two implementations: if implementation A's single measurement happens to land on a naturally faster moment and implementation B's happens to land on a slower one, you can easily conclude 'A is faster' when the two are actually comparable, or even when B is genuinely faster in typical conditions. This is precisely the trap that makes ad-hoc time.time() comparisons a common source of incorrect performance conclusions, even among engineers who know better in principle.

The fix isn't a smarter single measurement — it's *repetition*: running the same code many times and looking at the aggregate (average, or better, median/minimum) rather than trusting any one run in isolation, which is exactly what a dedicated benchmarking tool automates for you.

āœ•
—
+
import time

start = time.time()
result = sorted(large_list)
elapsed = time.time() - start
print(elapsed)  # run this 5 times -- you'll get 5 DIFFERENT numbers
localhost:3000
Measurement Noise
time.time() run 5 times
5 different numbers — noise, not a reliable signal

2timeit: Automated Repetition for a Stable Estimate

timeit.timeit(statement, setup=..., number=1000) runs the given statement the specified number of times (1000, here) in a tightly controlled loop, and returns the *total* time for all those runs — dividing by number gives you the average per-call time, a number that's dramatically more stable and reproducible than any single measurement, since random noise sources tend to average out across a large number of repetitions.

The setup parameter matters specifically because it lets you exclude one-time setup cost (building large_list, in the example) from the timed portion — you want to measure sorted(large_list)'s cost specifically, not sorted(large_list) plus the cost of constructing large_list in the first place, which would happen only once if written naively but would otherwise contaminate every one of the 1000 timed iterations if included inside the timed statement itself.

timeit also deliberately disables Python's automatic garbage collection during its timed runs by default — garbage collection can trigger unpredictably and add noise-like variance to timing measurements, so timeit controls for it specifically to produce a more consistent, comparable number across repeated runs (and across separate timeit.timeit() calls being compared against each other).

āœ•
—
+
import timeit

setup = "large_list = list(range(100_000))"
time_taken = timeit.timeit("sorted(large_list)", setup=setup, number=1000)
print(f"{time_taken / 1000:.6f}s per call, averaged over 1000 runs")
localhost:3000
Averaged Measurement
timeit.timeit(..., number=1000) / 1000
Stable per-call average, not a single noisy sample

3Fair Comparison: Identical Conditions, Same Input, Same Machine State

Benchmarking two implementations meaningfully against each other requires holding every variable *except* the code being compared constant: the same input data (data = list(range(10_000)), shared identically via setup for both timed statements), the same number of repetitions, run on the same machine in roughly the same conditions (ideally back-to-back, minimizing the chance that unrelated system load shifted meaningfully between the two benchmark runs).

The list-comprehension-versus-map() comparison in the scene is a genuinely common real question professional Python engineers have opinions about, and it's precisely the kind of question that should be settled by measurement, not intuition or a blog post's claim from a different Python version — timeit run identically for both candidates gives you an actual, current, reproducible answer for your specific interpreter version and workload shape, which is the only way to know for certain, since relative performance between approaches can and does shift across Python versions as the interpreter itself evolves (recall the JIT and adaptive interpreter improvements from the What's New in Python 3.13+ lesson).

The discipline this section builds toward, tying profiling, memory profiling, and benchmarking together: performance claims in professional Python work should be backed by a specific, reproducible measurement — 'I profiled this and cumulative time showed X', 'I benchmarked both implementations with timeit and A was Y% faster on this workload' — rather than general folklore about what's supposedly fast or slow in Python, which is frequently outdated, workload-dependent, or simply wrong for your specific case.

āœ•
—
+
import timeit

setup = "data = list(range(10_000))"
list_comp_time = timeit.timeit("[x**2 for x in data]", setup=setup, number=1000)
map_time = timeit.timeit("list(map(lambda x: x**2, data))", setup=setup, number=1000)

print(f"List comprehension: {list_comp_time:.4f}s")
print(f"map():              {map_time:.4f}s")
localhost:3000
Controlled Comparison
Same setup, same number=1000, both timed identically
A fair, reproducible comparison

4Step-by-Step Breakdown

Wrapping code in time.time() before and after feels like benchmarking. It usually isn't — a single run is dominated by noise you haven't accounted for.

A single time.time() measurement is noisy -- OS scheduling, caching, and background processes all add variance a single run can't average out.

Checkpoint: Why does a single time.time() measurement typically give a different number on each run of the exact same code?

  • →OS scheduling, caching effects, and background system activity introduce variance a single measurement cannot average out
  • →Python's interpreter itself runs code at a randomly varying speed by design

timeit.timeit() runs code MANY times automatically and reports something far more stable and comparable than a single measurement.

Checkpoint: What does running code 1000 times and averaging (as timeit does) accomplish that a single measurement cannot?

  • →It averages out random measurement noise, producing a far more stable, representative estimate of the code's true typical speed
  • →The code itself runs measurably faster the more times it is repeated

To compare two implementations fairly, benchmark both under IDENTICAL conditions -- same input, same machine state, same run count.

Benchmarking gives you rigorous measurement; Performance Optimization closes this section with the concrete techniques you apply once profiling and benchmarking have told you where to focus.

Average Real Benchmark Runs. Finish average_of_runs(): averaging several runs smooths out single-measurement noise.

Level Up šŸš€

Advanced cheat sheets, SEO tricks, and interview prep for this topic.

Browser Support

ChromeSupported

Fully supported (via server-side Python execution).

FirefoxSupported

Fully supported (via server-side Python execution).

SafariSupported

Fully supported (via server-side Python execution).

EdgeSupported

Fully supported (via server-side Python execution).

Best Practices

Never trust a single time.time() measurement for a performance comparison

Measurement noise from OS scheduling, caching, and system load can easily exceed the actual difference between two implementations — always use timeit's repeated-and-averaged approach for a comparison you intend to act on.

Keep setup code (data construction) out of the timed statement itself

Use timeit's setup parameter for one-time data preparation, so you measure only the operation you actually care about, not the cost of building its input repeated needlessly across every iteration.

Frequent Bugs

THE BUG

Comparing two implementations using a single time.time() measurement each, and concluding one is faster based on a difference that's actually within normal measurement noise.

THE FIX

Use timeit.timeit() with a meaningful repetition count for both implementations under identical conditions, and trust the averaged result over any single-run comparison.

Real-World Examples

Settling a Team Debate About String Concatenation Approaches

A code review debate breaks out over whether string concatenation with + in a loop or ''.join() on a list is meaningfully faster for building a large string, and the team wants a definitive, current answer rather than relying on outdated folklore.

import timeit

setup = "parts = [str(i) for i in range(10_000)]"

concat_time = timeit.timeit(
    "result = ''\nfor p in parts: result += p",
    setup=setup, number=100
)
join_time = timeit.timeit("''.join(parts)", setup=setup, number=100)

print(f"Concatenation: {concat_time:.4f}s")
print(f"join():        {join_time:.4f}s")

Interview Prep

?Frequently Asked Questions

Pascual Vila

Pascual Vila

Frontend Instructor // Code Syllabus

Common Pitfalls & Errors

The Error //

Including one-time setup/data-construction code inside the timed statement itself rather than in timeit's setup parameter, inflating the measured per-call time with cost that shouldn't be part of the comparison.

# Wrong: data construction repeated (and timed) on every iteration timeit.timeit("data = list(range(10000)); sorted(data)", number=1000) # Correct: setup runs once, only sorted(data) is actually timed timeit.timeit("sorted(data)", setup="data = list(range(10000))", number=1000)

The Solution //

Move data preparation and other one-time costs into the setup parameter, so the timed statement measures only the specific operation being benchmarked.

Lesson Glossary

[01]timeit

Python's standard library module for accurately timing small code snippets by running them repeatedly and reporting stable, averaged results.

Code Preview
// timeit context

[02]Measurement noise

Random variance in a timing measurement caused by external factors like OS scheduling, caching, and system load, unrelated to the code's true performance.

Code Preview
// Measurement noise context

[03]setup parameter (timeit)

A timeit.timeit() argument for one-time preparation code, excluded from the timed portion of the benchmark.

Code Preview
// setup parameter (timeit) context

[04]Fair comparison

A benchmarking methodology holding input data, repetition count, and environment constant between two implementations being compared.

Code Preview
// Fair comparison context

Continue Learning