Python cProfile example: find slow functions and read pstats
Profile a function, sort cumulative time, and distinguish call counts from elapsed time.
Example check: Worked and guided code samples are checked against CPython 3.13. Practice uses Pyodide 0.29.5 (Python 3.13) in your browser. How we check lessons
Video lesson: Python cProfile example: find slow functions and read pstats
The preview is stored on this site. YouTube loads only when you press Play. Watch on YouTube
Jump to a chapter
Read the video transcript
Select a timestamp to watch that moment on YouTube.
- 00:00
A script feels slow. Which function is responsible? cProfile can collect call counts and timing for a representative run. We will use a tiny wrapper and inner function to learn the columns, then show how recursion changes call counts. The exact seconds on your computer will differ, so the exercise checks stable structural facts rather than comparing arbitrary millisecond values. Keep a specific workload question in mind before opening a profile report. A Python cProfile example should answer a concrete question: which functions consume time during a reproducible workload? cProfile is in the standard library.
- 00:41
Run `python -m cProfile -s cumulative script.py` from a local terminal to profile an entire script, or create cProfile.Profile() and call runcall for a specific function. The result records function calls and timings. This lesson uses tiny deterministic workloads for the browser exercise, so it checks the call-count structure rather than claiming a stable millisecond benchmark. A profiler is useful when you can name a workload whose speed matters. Choose a representative input and run the same operation under cProfile. From a terminal, `python -m cProfile -s cumulative script.py` prints a sorted whole-script report.
- 01:23
Inside Python, Profile.runcall focuses on one function. Tiny examples are good for learning the API, but their timing columns cannot justify a production optimization. The first profile sorts by total self time. That can be useful, but if our question is which high-level path dominates, a wrapper may look deceptively cheap because most work is in its subcall. This is a diagnostic choice, not an exception. Read the column label before deciding what the numbers mean. A top row sorted by one metric does not automatically identify the algorithm to change. We need to compare inclusive and exclusive time.
- 02:05
In a profile report, `ncalls` counts all calls and may show a total/primitive form for recursion. `tottime` excludes time in subcalls, while `cumtime` includes them. Sorting by cumulative time helps find higher-level functions that dominate a program. Sorting by total time highlights work done inside a function itself. The top row is not automatically the root cause; inspect what that function calls and whether the workload represents the real application. A profile is measurement evidence, not a replacement for a clear performance question. This guide starts with one function and verifies that it appears in pstats.
- 02:46
We avoid asserting a number of milliseconds because operating system scheduling and runtime state change measurements. The exercise still uses the real cProfile module, so the structure of the report is authentic. For your own program, profile a workload large enough that repeated runs reveal a stable pattern. The diagnosis profiles a recursive countdown and prints the primitive and total call counts. One top-level invocation triggers four calls: values three, two, one, and zero. A report may display a total/primitive form for such a function. This is a stable fact that a task checker can verify.
- 03:27
It does not say anything useful about speed because four tiny calls are too fast to benchmark reliably. Understanding call structure helps interpret the larger timing report later. The programmatic API uses `profile.runcall(function, argument)` and `pstats.Stats(profile).sort_stats(SortKey.CUMULATIVE).print_stats(10)`. For stable tests, the code accesses the `stats` mapping and reads its call counters. In a real investigation, print the human- readable report and compare a representative input before and after a change. Avoid profiling only one tiny input and extrapolating to a million rows: algorithm
- 04:08
growth can be more important than one measured duration. The report's `tottime` is time spent in a function excluding its subcalls. `cumtime` includes subcalls and is often useful for locating high-level slow paths. `ncalls` counts invocations; a recursive function can show total and primitive counts. Use `sort_stats(SortKey.CUMULATIVE)` to order by inclusive cost, and compare with `SortKey.TIME` when you need to locate self time. Now sort by SortKey.CUMULATIVE. This includes time spent in subcalls, so the wrapper row can reflect the work it initiated. Compare with tottime, which excludes callees.
- 04:50
For a real script, run python -m cProfile -s cumulative your_script.py in a local terminal or write the report to a profile file for pstats. Profile a workload that resembles actual use; tiny synthetic input can conceal a costly growth pattern. The practical task counts calls to a named function, including recursive calls, without asserting any elapsed time. The sample recursive function is run once at the outer level but appears multiple times in `ncalls`. The final examples compare a direct call and recursion, then show how to sort a report. When you are ready to profile your own script, run it locally with realistic data
- 05:32
and keep the profile separate from a timeit microbenchmark. For an end-to-end workflow, first locate the slow path, then write a correctness test, change one implementation detail, and profile again under the same conditions. The pstats stats mapping is also available programmatically, but the human-readable report is easier to inspect during a performance investigation. Here we read the second tuple element to test total calls deterministically. A high call count does not prove a problem: a cheap function may be called many times, while a single expensive call may dominate time. Interpret the columns together.
- 06:13
The final run implements a count_calls helper and confirms four recursive invocations. On your own program, after locating a slow path, write a correctness test before changing it and profile both versions with the same input. If one function appears frequently but takes almost no time, it may not be the best target. If a wrapper has high cumulative time but low self time, inspect its callees. Pair these observations with algorithm reasoning rather than treating one profile snapshot as universal truth. Recursion demonstrates why a single top- level call can appear many times in the report.
- 06:53
A countdown from three invokes the same function at three, two, one, and zero: four total calls. Counting those calls provides a stable task assertion. The actual elapsed time for four calls is too small and variable to teach a reliable optimization conclusion. After locating a suspicious path in your own program, preserve correctness tests before changing the implementation. Re-profile with the same input and compare the same columns. If input size changes, a difference may come from workload rather than code. Pair a profile with algorithm reasoning and a benchmark only when you have a specific hypothesis about why one path is slow.
Understand the concept
A Python cProfile example should answer a concrete question: which functions consume time during a reproducible workload? cProfile is in the standard library. Run `python -m cProfile -s cumulative script.py` from a local terminal to profile an entire script, or create cProfile.Profile() and call runcall for a specific function. The result records function calls and timings. This lesson uses tiny deterministic workloads for the browser exercise, so it checks the call-count structure rather than claiming a stable millisecond benchmark.
In a profile report, `ncalls` counts all calls and may show a total/primitive form for recursion. `tottime` excludes time in subcalls, while `cumtime` includes them. Sorting by cumulative time helps find higher-level functions that dominate a program. Sorting by total time highlights work done inside a function itself. The top row is not automatically the root cause; inspect what that function calls and whether the workload represents the real application. A profile is measurement evidence, not a replacement for a clear performance question.
The programmatic API uses `profile.runcall(function, argument)` and `pstats.Stats(profile).sort_stats(SortKey.CUMULATIVE).print_stats(10)`. For stable tests, the code accesses the `stats` mapping and reads its call counters. In a real investigation, print the human-readable report and compare a representative input before and after a change. Avoid profiling only one tiny input and extrapolating to a million rows: algorithm growth can be more important than one measured duration.
The practical task counts calls to a named function, including recursive calls, without asserting any elapsed time. The sample recursive function is run once at the outer level but appears multiple times in `ncalls`. The final examples compare a direct call and recursion, then show how to sort a report. When you are ready to profile your own script, run it locally with realistic data and keep the profile separate from a timeit microbenchmark. For an end-to-end workflow, first locate the slow path, then write a correctness test, change one implementation detail, and profile again under the same conditions.
- cProfile records calls and timing
- cumtime includes subcalls
- tottime excludes subcalls
- pstats sorts and filters a report
See it step by step
Read the code, predict the output, then compare it with the result.
01. Profile one named function
import cProfile, pstats
def square_total(n): return sum(x*x for x in range(n))
profile = cProfile.Profile()
profile.runcall(square_total, 10)
print(any(name == 'square_total' for _,_,name in pstats.Stats(profile).stats))TrueThe stats mapping contains a row for the profiled function; exact timing is deliberately omitted.
02. Read the total call count
import cProfile, pstats
def identity(value): return value
profile = cProfile.Profile()
profile.runcall(identity, 3)
print(sum(value[1] for (_,_,name),value in pstats.Stats(profile).stats.items() if name == 'identity'))1The second statistics value is the total call count, which is stable for this example.
03. Count recursion without measuring seconds
import cProfile, pstats
def descend(n): return 0 if n == 0 else descend(n-1)
profile = cProfile.Profile()
profile.runcall(descend, 3)
print(sum(value[1] for (_,_,name),value in pstats.Stats(profile).stats.items() if name == 'descend'))4The outer call plus three recursive calls produce four total calls.
Run the complete example locally
Run a real script from a local terminal with python -m cProfile -s cumulative your_script.py. The browser examples use a small standard-library Profile object and check call counts; timing values vary across machines.
import cProfile, pstats
from pstats import SortKey
def total_squares(n):
return sum(i * i for i in range(n))
profile = cProfile.Profile()
profile.runcall(total_squares, 10000)
pstats.Stats(profile).sort_stats(SortKey.CUMULATIVE).print_stats(8)A pstats table sorted by cumtime. Timings and row order beyond the sort key vary by machine; inspect total_squares and its subcalls.A closer look
Follow the reasoning, inspect each result, then try the suggested changes in the console below.
Profile a real workload
A profiler is useful when you can name a workload whose speed matters. Choose a representative input and run the same operation under cProfile. From a terminal, `python -m cProfile -s cumulative script.py` prints a sorted whole-script report. Inside Python, Profile.runcall focuses on one function. Tiny examples are good for learning the API, but their timing columns cannot justify a production optimization.
This guide starts with one function and verifies that it appears in pstats. We avoid asserting a number of milliseconds because operating system scheduling and runtime state change measurements. The exercise still uses the real cProfile module, so the structure of the report is authentic. For your own program, profile a workload large enough that repeated runs reveal a stable pattern.
import cProfile, pstats
def square_total(n): return sum(x*x for x in range(n))
profile = cProfile.Profile()
profile.runcall(square_total, 10)
print(any(name == 'square_total' for _,_,name in pstats.Stats(profile).stats))True- Choose one named function and a representative input.
- Run it through cProfile.Profile().runcall.
Read total and cumulative time
The report's `tottime` is time spent in a function excluding its subcalls. `cumtime` includes subcalls and is often useful for locating high-level slow paths. `ncalls` counts invocations; a recursive function can show total and primitive counts. Use `sort_stats(SortKey.CUMULATIVE)` to order by inclusive cost, and compare with `SortKey.TIME` when you need to locate self time.
The pstats stats mapping is also available programmatically, but the human-readable report is easier to inspect during a performance investigation. Here we read the second tuple element to test total calls deterministically. A high call count does not prove a problem: a cheap function may be called many times, while a single expensive call may dominate time. Interpret the columns together.
import cProfile, pstats
def identity(x): return x
profile = cProfile.Profile()
profile.runcall(identity, 7)
print(sum(values[1] for (_,_,name),values in pstats.Stats(profile).stats.items() if name == 'identity'))1- Read call count from the stats mapping.
- Inspect cumulative versus total time in the printed local report.
Verify recursion, then compare changes
Recursion demonstrates why a single top-level call can appear many times in the report. A countdown from three invokes the same function at three, two, one, and zero: four total calls. Counting those calls provides a stable task assertion. The actual elapsed time for four calls is too small and variable to teach a reliable optimization conclusion.
After locating a suspicious path in your own program, preserve correctness tests before changing the implementation. Re-profile with the same input and compare the same columns. If input size changes, a difference may come from workload rather than code. Pair a profile with algorithm reasoning and a benchmark only when you have a specific hypothesis about why one path is slow.
import cProfile, pstats
def descend(n): return 0 if n == 0 else descend(n-1)
profile = cProfile.Profile()
profile.runcall(descend, 3)
print(sum(values[1] for (_,_,name),values in pstats.Stats(profile).stats.items() if name == 'descend'))4- Count total recursive invocations.
- Keep input and environment comparable before and after an optimization.
Try it in Python
Edit the example and run it. Python starts in your browser the first time you click Run.
Python console
Ready to runNeed input()? Add one value per line
Your output appears here.
Need a hint?
Each pstats stats entry has a (filename, line, function_name) key. Its value's second element is total calls; sum matching entries after profile.runcall.
Complete the task and select Check task to verify your code.
Quick quiz
Three questions. You can change your answers and try again.
Typical mistakes
Everyone meets these errors. See what causes them and how to fix them.
Reading cumulative time as self time
cumtime == time spent only in this function's own linesCompare cumtime with tottime and inspect callees.What happens: A wrapper function is blamed for work in its subcalls.
A profiler report separates inclusive and exclusive time.
Profiling an unrepresentative input
profile a list of three items and extrapolateProfile representative inputs and compare under the same conditions.What happens: An algorithm that scales poorly appears harmless.
Input size and shape affect where time is spent.