Python Measure Execution Time: time, perf_counter, and timeit

Learn how to measure execution time in Python using time.perf_counter(), timeit, process_time(), and decorators, with examples in seconds, milliseconds, and nanoseconds.

Published

Updated

Read time 6 min read

Reviewed byDeepak Prasad

Python Measure Execution Time: time, perf_counter, and timeit

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:

python
from time import perf_counter

t0 = perf_counter()
total = sum(range(50_000))
t1 = perf_counter()
print(total, t1 - t0)
Output

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.


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:

python
import time
from time import perf_counter

t0 = perf_counter()
time.sleep(0.05)
elapsed = perf_counter() - t0
print(f"Elapsed: {elapsed:.4f} seconds")
Output

You should see roughly 0.05 seconds plus a small overhead from the interpreter and scheduling.


Multiply the same delta by 1000 and pick a precision that suits your logs:

python
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")
Output

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+):

python
from time import perf_counter_ns

t0 = perf_counter_ns()
n = sum(range(10_000))
t1 = perf_counter_ns()
print(n, t1 - t0, "ns")
Output

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:

python
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")
Output

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:

python
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))
Output

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:

python
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")
Output

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.

python
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)
Output

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 timeit with enough number or 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


Frequently Asked Questions

1. What is the best way to measure elapsed time in Python?

Use time.perf_counter() (or perf_counter_ns()) for wall-clock elapsed time between two points; it is monotonic and designed for intervals, unlike time.time() which can move backward if the system clock is adjusted.

2. When should I use timeit instead of perf_counter?

Use the timeit module to benchmark small expressions or statements with controlled repetition and optional setup; it reduces timer overhead and is better than a single hand-timed run for micro-benchmarks.

3. When should I use process_time() instead of perf_counter()?

Use time.process_time() (or process_time_ns()) when you need CPU time charged to the current process, excluding time spent sleeping or waiting on I/O; it answers how much processor work you did, not how long a wall clock waited.

4. How do I print execution time in milliseconds?

Subtract two perf_counter() readings to get seconds as a float, then multiply by 1000 and format with an f-string, for example f"{(t1 - t0) * 1000:.3f} ms".
Azka Iftikhar

Computer Scientist

Proficient in multiple programming languages, including C++, Golang, and Java, she brings a versatile skillset to tackle a wide array of challenges. Experienced Computer Scientist and Web Developer, …