Benchmarking Python Decorator Overhead with timeit
Reviewed
A single 10 ns difference is not enough to establish a decorator's overhead. Timer resolution, scheduling noise, Python version, browser execution, and the wrapper's implementation all affect the result. Use repeated timeit measurements of bare and wrapped calls, compare their per-call difference, and report the environment rather than treating 10 ns or 200 ns as universal figures.
Asked on Stack Overflow Open this example in the editorOpens as a new program; running code requires JavaScript.
Run the repeated-call comparison
Run it a few times. The absolute numbers move from run to run — the difference is the point. The demo prints three ns/call lines; it does not promise any particular numbers. Browser results measure CPython through WebAssembly, not native CPython on your own machine.
import timeit
def deco(f):
def wrapper(*a, **k):
return f(*a, **k)
return wrapper
def work(x):
return x + 1
wrapped = deco(work)
n = 1_000_000
bare = min(timeit.repeat(lambda: work(1), number=n, repeat=5)) / n * 1e9
wrap = min(timeit.repeat(lambda: wrapped(1), number=n, repeat=5)) / n * 1e9
print(f"bare: {bare:.1f} ns/call")
print(f"wrapped: {wrap:.1f} ns/call")
print(f"decorator cost: {wrap - bare:.1f} ns/call")Measure batches, not one function call
Use timeit rather than one perf_counter() pair around a single call. This sample runs each callable 1,000,000 times per measurement and repeats five times. Dividing by the iteration count reports per-call time. A clock's reported unit is not a guarantee of nanosecond resolution or accuracy; inspect time.get_clock_info('perf_counter') for the environment.
Taking min(timeit.repeat(...)) estimates the lower-bound execution cost with less influence from pauses that only make a trial slower. Keep the full spread when judging stability; a minimum does not prove all noise vanished. timeit temporarily disables cyclic garbage collection by default, so there is no need to add separate GC management for this sample.
The lambda harness exists in both measurements, so taking wrapped minus bare is intended to isolate the wrapper cost. Measurements occur separately, however, so drift, warm-up, and CPU state need not cancel. Reorder and repeat comparisons before drawing conclusions.
Treat tiny timing claims as environment-specific
A difference below roughly 50 ns deserves extra scrutiny, not an automatic declaration that it is impossible. A single noisy measurement cannot support it; repeated batches on a controlled machine may resolve small average differences. Repeat on an idle machine and control CPU frequency when precision matters. Browser timer precision and throttling can differ from native Python.
Bare-call estimates around 50–100 ns and extra-wrapper estimates around 60–150 ns are illustrative native-Python ballparks, not measurements from this guide and not guarantees for Pyodide. A roughly 200 ns wrapper cost can be plausible on some systems; a claimed 10 ns runtime type-checking overhead needs a reproducible benchmark and a clear definition of what was compared.
Profile before optimizing a wrapper
If overhead were 200 ns and the function did 1 ms of useful work, the extra cost would be 0.02%. Wrapper overhead is most likely to matter when a trivial function is called millions of times in a hot loop.
Profile first with cProfile, or py-spy in an appropriate native environment. py-spy is not a tool installed in this browser editor. Check that time spent in decorators is significant before making code harder to maintain for a microbenchmark win.