Read time.perf_counter() before and after, and subtract.
import time start = time.perf_counter() total = sum(n * n for n in range(1_000_000)) elapsed = time.perf_counter() - start print(f"took {elapsed:.3f} seconds")
took 0.065 seconds
perf_counter is the clock built for this: it has the highest resolution available and never jumps. time.time() follows the wall clock, which can move backwards when your computer syncs its time, and then a measurement comes out negative.
Timing a function every time it runs
A small decorator saves pasting the same three lines everywhere:
import time from functools import wraps def timed(func): @wraps(func) def wrapper(*args, **kwargs): start = time.perf_counter() result = func(*args, **kwargs) print(f"{func.__name__} took {time.perf_counter() - start:.4f}s") return result return wrapper @timed def build_report(rows): return sorted(range(rows), reverse=True)[:5] print(build_report(200_000))
setup added so this can run · defines args, kwargs
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) args = _AutoMock('args') kwargs = _AutoMock('kwargs')
build_report took 0.0052s [199999, 199998, 199997, 199996, 199995]
Comparing two ways of doing something: timeit
A single run of a fast snippet is mostly noise. timeit runs each version many times so the comparison is fair. Compare the time per run, which is why each total is divided by its repeat count:
import timeit items = list(range(10_000)) as_set = set(items) # A set lookup is so quick that a thousand of them can finish inside one tick of # the browser's clock, so the set gets far more repeats before dividing out. per_list = timeit.timeit(lambda: 9_999 in items, number=1_000) / 1_000 per_set = timeit.timeit(lambda: 9_999 in as_set, number=500_000) / 500_000 print(f"list: {per_list * 1e6:.1f} µs per lookup set: {per_set * 1e6:.3f} µs per lookup") print(f"the set is about {per_list / per_set:,.0f}x faster here")
list: 63.4 µs per lookup set: 0.045 µs per lookup the set is about 1,408x faster here
Those numbers are from a desktop. Yours will differ, and in the browser everything runs slower and even the ratio moves. What holds everywhere is the size of the gap: the set wins by orders of magnitude, not by a few per cent. In a Jupyter notebook, %timeit expression does all of this in one line.
Where is the time going?
When a whole program is slow and you don't know which part, timing pieces by hand is guesswork. python -m cProfile -s cumtime your_script.py lists every function with the time spent inside it, slowest first. The performance lesson walks through reading that output.