How to find out why Python code is slow before trying to optimise it: timing snippets with timeit and perf_counter, profiling whole programs with cProfile and pstats, reading the output, visualising results, and common fixes.
The first rule of speeding up code is to measure before you change anything. Guessing where the slow part is usually leads to optimising the wrong thing. Python's standard library includes the tools you need.
Quick timing with perf_counter
from time import perf_counter
start = perf_counter()
result = do_work()
print(f"took {perf_counter() - start:.3f}s")
perf_counter is a high-resolution clock suited to measuring durations. Use it for a rough idea of how long a step takes.
Comparing small snippets with timeit
timeit runs code many times and reports the best result, reducing noise from other activity on your computer.
python -m timeit "sum(range(1000))"
python -m timeit -s "data = list(range(1000))" "[x*2 for x in data]"
The -s option runs setup code once, so it isn't included in the timing. From Python code:
import timeit
print(min(timeit.repeat("'-'.join(map(str, range(100)))", number=10_000, repeat=5)))
In Jupyter or IPython, the %timeit magic does the same.
Profiling a whole program with cProfile
When you don't know which part is slow, profile:
python -m cProfile -s cumulative my_script.py
Key columns in the output:
- ncalls: how many times a function was called;
- tottime: time spent in the function itself, excluding functions it called;
- cumtime: total time including everything it called.
Sort by cumulative to find the expensive high-level steps, then by tottime to find the specific functions doing the work.
Saving and exploring results
python -m cProfile -o profile.out my_script.py
python -c "import pstats; pstats.Stats('profile.out').sort_stats('tottime').print_stats(15)"
Third-party viewers such as SnakeViz show the same data as an interactive chart. For line-by-line detail, line_profiler can time each line of a chosen function. Sampling profilers such as py-spy can attach to a running program with little overhead.
Common causes of slow Python, and fixes
- Looking things up in a list:
x in my_listchecks every item. Use a set or dict for membership tests. - Repeated work in loops: move constant calculations, regex compilation and attribute lookups out of the loop.
- Building strings with += in large loops: collect parts in a list and use
"".join(). - Pure-Python number crunching: NumPy, pandas or Polars do bulk numeric work far faster.
- Waiting on input/output: if the profile shows time in network or disk calls, batching requests, caching, or concurrency (threads or asyncio) helps more than code tweaks.
- Repeating expensive calls:
functools.lru_cacheorfunctools.cachememoises results of pure functions.
A worked example
Suppose a script that matches customer emails against a list of 50,000 unsubscribed addresses takes minutes. Profiling shows almost all the tottime inside the loop that checks if email in unsubscribed, where unsubscribed is a list. Each check scans the whole list. Converting it once with unsubscribed = set(unsubscribed) makes each lookup take roughly constant time, and the script finishes in seconds. Re-running the profile confirms the hotspot has gone, and the tests confirm the output hasn't changed. That's the pattern: measure, fix the biggest hotspot, measure again.
Good habits
- Profile with realistic data: small test inputs can hide the real bottleneck.
- Change one thing at a time and measure again.
- Keep tests passing: faster wrong answers aren't an improvement.
- Stop when it's fast enough. Readability matters too.
Frequently asked questions
Does profiling slow my program down?
cProfile adds overhead, so absolute times are inflated, but the relative picture is still useful.
Will a newer Python version be faster?
Often. Recent releases have included significant speed improvements, so upgrading to a supported version is worth testing.
Sources and further reading
- Python docs: The Python profilers (cProfile, pstats)
- Python docs: timeit
- Python docs: time.perf_counter
- Python docs: functools.lru_cache
Facts checked on 20 October 2026. Spotted an error? Tell us and we will correct it.
