Use the Python `cProfile` Module

Learn how to use Python's cProfile module to profile your code, find performance bottlenecks, and measure function call statistics.

6 min read

The cProfile module in the Python standard library measures how long your code takes to run and which functions consume the most time. Profiling answers the question "why is my program slow?" with data instead of guesswork. Use cProfile before optimizing to focus effort on the code that actually matters, and combine it with the pdb debugger when you need to understand not just what is slow but why.

Run it from the command line without changing any of your source code, which is the fastest way to get a first look at where time goes:

bashbash
python -m cProfile -s tottime myscript.py

Or call it directly inside your program when you only want to profile a specific piece of code rather than the whole script:

pythonpython
import cProfile
 
def slow_function():
    total = 0
    for i in range(1_000_000):
        total += i
    return total
 
cProfile.run("slow_function()", sort="tottime")

Running this prints a full statistics table, showing exactly one function call to slow_function that accounts for essentially all of the runtime:

texttext
         5 function calls in 0.042 seconds
 
   Ordered by: internal time
 
   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.042    0.042    0.042    0.042 <string>:1(slow_function)
        1    0.000    0.000    0.042    0.042 <string>:1(<module>)
        1    0.000    0.000    0.042    0.042 {built-in method builtins.exec}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}

cProfile.run() executes the code string and prints a statistics table right after it finishes. sort="tottime" sorts the rows by time inside each function, excluding time spent in subcalls, so the biggest self-contained bottleneck appears first.

Understanding the profiling columns

Each row in the output represents one function. The columns tell you where time is spent.

ColumnMeaning
ncallsNumber of calls. 3/1 means 3 total, 1 primitive
tottimeTotal time in the function, excluding subcalls
percalltottime / ncalls
cumtimeTotal time in the function, including subcalls
percallcumtime / ncalls
filename:lineno(function)Where the function is defined

Focus on functions with high tottime first: these are where the function itself does the actual work, rather than delegating to something else. If cumtime is much larger than tottime, the function is mostly calling other slow functions, so the real bottleneck is further down the call chain.

Profiling from the command line

The most common way to profile is directly from the terminal.

bashbash
python -m cProfile -s cumulative my_script.py

Sort options:

FlagSorts by
-s tottimeTime in the function itself
-s cumtimeTotal time including subcalls
-s ncallsNumber of calls
-s nameFunction name alphabetically

Save the output to a file for later analysis with pstats:

bashbash
python -m cProfile -o output.prof my_script.py

Analyzing results with pstats

The pstats module reads saved profiling data and lets you sort, filter, and inspect it interactively.

pythonpython
import cProfile
import pstats
from io import StringIO
 
def work():
    total = sum(range(10_000_000))
    squares = [x * x for x in range(100_000)]
    return total + sum(squares)
 
profiler = cProfile.Profile()
profiler.enable()
work()
profiler.disable()

With profiling data collected, feed it into pstats.Stats to sort and format the results as a readable table, writing the output to an in-memory stream:

pythonpython
stream = StringIO()
stats = pstats.Stats(profiler, stream=stream)
stats.sort_stats("tottime")
stats.print_stats(10)
print(stream.getvalue())

work() itself accounts for the vast majority of the time, since the sum() and list comprehension calls it makes are comparatively cheap:

texttext
         7 function calls in 0.385 seconds
 
   Ordered by: internal time
 
   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.341    0.341    0.385    0.385 <stdin>:1(work)
        1    0.024    0.024    0.024    0.024 <stdin>:2(<listcomp>)
        1    0.020    0.020    0.020    0.020 {built-in method builtins.sum}

print_stats(N) limits output to the top N functions. Use sort_stats() to change the sort order and print_stats() to re-display.

For saved profiles:

pythonpython
import pstats
 
stats = pstats.Stats("output.prof")
stats.sort_stats("tottime")
stats.print_stats(20)

Profiling specific code sections

Use enable() and disable() to profile only the code you care about, skipping setup work and cleanup steps that would otherwise pad the numbers.

pythonpython
import cProfile
 
profiler = cProfile.Profile()
 
setup_code()
profiler.enable()
result = expensive_operation()
profiler.disable()
cleanup_code()
 
profiler.print_stats(sort="tottime")

This ignores setup and cleanup overhead, focusing the profile on the expensive operation.

Practical example: finding and fixing a bottleneck

Profile a function, identify the slow part, and verify the fix. before() builds an intermediate list, while after() computes the same sum with a generator expression.

pythonpython
import cProfile
import pstats
from io import StringIO
 
def before():
    result = []
    for i in range(10_000):
        result.append(i ** 2)
    return sum(result)
 
def after():
    return sum(i ** 2 for i in range(10_000))

Profile both versions back to back so their statistics can be compared directly, using the same profiling setup for each to keep the comparison fair:

pythonpython
for label, func in [("Before", before), ("After", after)]:
    profiler = cProfile.Profile()
    profiler.enable()
    func()
    profiler.disable()
 
    stream = StringIO()
    stats = pstats.Stats(profiler, stream=stream)
    stats.sort_stats("tottime")
    stats.print_stats(5)
    print(f"--- {label} ---")
    print(stream.getvalue())

The generator expression version is typically faster because it avoids building an intermediate list. The profiler confirms which functions change between the two versions.

Common mistakes

Optimizing without profiling. Intuition about what is slow is often wrong. Profile first, then optimize only the functions that dominate the runtime.

Reading cumtime as the target for optimization. If a function has high cumtime but low tottime, the problem is in the functions it calls, not in the function itself. Drill into subcalls first.

Profiling code that is already fast enough. If your program runs in under a second, profiling may not be worth the effort. Focus profiling on code that takes seconds or minutes.

Forgetting that profiling adds overhead. cProfile is lightweight but not zero-cost. The measured times include a small profiling overhead. For micro-benchmarks, use timeit instead.

Rune AI

Rune AI

Key Insights

  • Run python -m cProfile -s tottime script.py to profile from the command line.
  • Use cProfile.run('code') to profile code in your program.
  • tottime is time inside the function; cumtime includes subcalls.
  • Use pstats.Stats to sort and filter profiling results programmatically.
  • Optimize functions with high tottime first, then those with high ncalls.
  • Profile before optimizing; intuition about what is slow is often wrong.
RunePowered by Rune AI

Frequently Asked Questions

What is the difference between cProfile and profile?

`cProfile` is written in C and has low overhead, making it suitable for profiling production code. `profile` is a pure Python implementation with higher overhead. Always prefer `cProfile` unless you need to extend or modify the profiler itself.

How do I profile a Python script from the command line?

Run `python -m cProfile -s cumulative myscript.py`. The `-s` flag sorts the output. Common sort keys: `cumulative` (total time in function + subcalls), `tottime` (time in function excluding subcalls), `ncalls` (number of calls), and `name` (function name).

What is the difference between tottime and cumtime in profiling output?

`tottime` is the time spent inside the function itself, excluding time in functions it called. `cumtime` is the total time including all subcalls. If `cumtime` is much larger than `tottime`, the function spends most of its time waiting for subcalls to complete.

Conclusion

cProfile is the standard tool for measuring Python performance. Run it from the command line for quick profiling, or use cProfile.run() and pstats for programmatic analysis. Focus optimization on functions with high tottime (time in the function itself), then on functions called many times. Always profile before optimizing.