This guide is for measuring how long code takes to run: wall-clock elapsed time for real-world waits, and CPU time when you care about processor work instead of calendar time. It centers on time.perf_counter(), adds timeit for tiny snippet benchmarks, and uses time.process_time() only where CPU accounting matters.
For how floating-point durations behave in arithmetic and printing, see Python numbers. To time code inside a reusable wrapper, pair perf_counter with Python functions and decorators; for loop-heavy snippets, see Python for loop patterns you might benchmark with timeit.
Tested on: Python 3.13.3; kernel 6.14.0-37-generic.
Best way to measure execution time in Python
For elapsed (wall-clock) time between two statements, prefer time.perf_counter() or time.perf_counter_ns(). Both use the highest-resolution monotonic clock available for timing, and perf_counter() is not affected by daylight saving or manual clock changes the way time.time() can be. Subtract a start reading from an end reading; the difference is seconds (float) or nanoseconds (int), respectively.
Measure execution time using time.perf_counter()
Import the name from the time module (or call time.perf_counter()). Take a timestamp before and after the work, then subtract:
from time import perf_counter
t0 = perf_counter()
total = sum(range(50_000))
t1 = perf_counter()
print(total, t1 - t0)The last value printed is the elapsed time in seconds as a float. Use the same pattern around any block: file I/O, network calls, or a heavy algorithm.
Print execution time in seconds
Format the float for humans with an f-string. This example uses a short sleep so the duration is easy to recognize on any machine:
import time
from time import perf_counter
t0 = perf_counter()
time.sleep(0.05)
elapsed = perf_counter() - t0
print(f"Elapsed: {elapsed:.4f} seconds")You should see roughly 0.05 seconds plus a small overhead from the interpreter and scheduling.
Print execution time in milliseconds
Multiply the same delta by 1000 and pick a precision that suits your logs:
import time
from time import perf_counter
t0 = perf_counter()
time.sleep(0.02)
ms = (perf_counter() - t0) * 1000
print(f"Elapsed: {ms:.2f} ms")For sub-millisecond work, three decimal places in milliseconds (or nanoseconds below) avoids noisy false precision.
Measure execution time in nanoseconds
perf_counter_ns() returns an integer nanosecond count, which avoids float rounding for very short intervals (Python 3.7+):
from time import perf_counter_ns
t0 = perf_counter_ns()
n = sum(range(10_000))
t1 = perf_counter_ns()
print(n, t1 - t0, "ns")The difference t1 - t0 is an integer nanosecond span. You can still convert to seconds with elapsed_ns / 1e9 when you need to compare with other float timings.
Measure function execution time
Wrap the call you care about with two readings. This keeps logic and timing separate:
from time import perf_counter
def work(n):
return sum(range(n))
start = perf_counter()
result = work(80_000)
end = perf_counter()
print(result, f"{(end - start) * 1000:.3f} ms")If you time many functions, copy-pasting those three lines gets noisy; a decorator centralizes it.
Measure execution time using a decorator
Use functools.wraps so the wrapped function keeps its name and docstring for debuggers and tests:
from functools import wraps
from time import perf_counter
def timed(label="elapsed"):
def decorator(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
t0 = perf_counter()
try:
return fn(*args, **kwargs)
finally:
print(f"{label}: {(perf_counter() - t0) * 1000:.3f} ms")
return wrapper
return decorator
@timed("sum_range")
def noisy_sum(n):
return sum(range(n))
print(noisy_sum(100_000))The finally block runs even if the function raises, so you still record time for failing paths.
Measure small code snippets using timeit
The timeit module runs a statement many times and returns the total or per-run time, which smooths out timer noise and scheduler jitter for micro-benchmarks. Pass the code as a string (or a callable in advanced APIs) plus a number of iterations:
import timeit
stmt = "sum(range(100))"
total = timeit.timeit(stmt, number=50_000)
print(total, "seconds for", 50_000, "runs")
print("per run", total / 50_000 * 1000, "ms")For reporting, timeit.repeat(stmt, repeat=5, number=n) runs several batches so you can take the min of the totals as a conservative estimate (still not a substitute for profiling real workloads).
Measure CPU execution time using process_time()
time.process_time() counts only CPU time attributed to the current process, not time spent blocked in sleep, waiting on locks, or in I/O that does not consume CPU. Use it when you want to compare CPU work (for example two algorithms with similar wall time but different load), not user-perceived latency.
import time
from time import perf_counter, process_time
wall0, cpu0 = perf_counter(), process_time()
time.sleep(0.05)
sum(range(150_000))
print("wall seconds", perf_counter() - wall0)
print("CPU seconds", process_time() - cpu0)Wall time includes the sleep; CPU time reflects mostly the sum loop. process_time_ns() is the integer nanosecond variant.
time.time() vs perf_counter() vs process_time() vs timeit
| API | What it measures | Typical use |
|---|---|---|
time.time() |
Seconds since the epoch (wall clock) | Timestamps, logging “what time is it,” not precise intervals |
time.perf_counter() |
Monotonic elapsed time with best resolution | Durations, benchmarks of real code paths |
time.process_time() |
CPU seconds for this process | CPU cost excluding sleep and some waits |
timeit |
Repeated execution of a small statement | Micro-benchmarks, comparing snippets |
time.monotonic() is also monotonic like perf_counter(); for simple deltas, perf_counter() is the usual choice because it may offer finer resolution.
Common mistakes when measuring execution time
- Using
time.time()for short intervals: the system clock can jump (NTP, manual adjustment), distorting deltas. - Drawing conclusions from one run of a tiny snippet: OS scheduling and cache effects dominate; use
timeitwith enoughnumberor repeat runs and report spread. - Measuring cold start and steady state together: first runs may include import or JIT effects; warm up before timing if you care about steady state.
- Forgetting that
process_time()ignores sleep: good for CPU accounting, misleading if you expected wall-clock total time. - Printing epoch seconds from
time.time()and calling it “execution time”: that value is a clock reading, not a duration.
Python execution time quick reference table
| Goal | Pattern |
|---|---|
| Wall elapsed seconds | t0 = perf_counter() … perf_counter() - t0 |
| Wall elapsed ns | t0 = perf_counter_ns() … t1 - t0 |
| Milliseconds display | (t1 - t0) * 1000 |
| CPU seconds | process_time() delta |
| Snippet benchmark | timeit.timeit("stmt", number=…) |
| Reusable timing | Decorator with perf_counter in finally |
Summary
Measure real-world elapsed time with time.perf_counter() or perf_counter_ns(), subtract start from end, and format as seconds or milliseconds for logs. Wrap functions with a small decorator when you want consistent timing without clutter. For tiny code fragments, use timeit with enough iterations instead of a single noisy measurement. When the question is how much CPU the process used—not how long the user waited—use time.process_time() or process_time_ns(). Reserve time.time() for absolute timestamps, not high-quality intervals.
References
- time — Time access and conversions (CPython,
perf_counter,process_time,monotonic) - timeit — Measure small code snippets

