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:
python -m cProfile -s tottime myscript.pyOr call it directly inside your program when you only want to profile a specific piece of code rather than the whole script:
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:
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.
| Column | Meaning |
|---|---|
| ncalls | Number of calls. 3/1 means 3 total, 1 primitive |
| tottime | Total time in the function, excluding subcalls |
| percall | tottime / ncalls |
| cumtime | Total time in the function, including subcalls |
| percall | cumtime / 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.
python -m cProfile -s cumulative my_script.pySort options:
| Flag | Sorts by |
|---|---|
| -s tottime | Time in the function itself |
| -s cumtime | Total time including subcalls |
| -s ncalls | Number of calls |
| -s name | Function name alphabetically |
Save the output to a file for later analysis with pstats:
python -m cProfile -o output.prof my_script.pyAnalyzing results with pstats
The pstats module reads saved profiling data and lets you sort, filter, and inspect it interactively.
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:
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:
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:
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.
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.
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:
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
Key Insights
- Run
python -m cProfile -s tottime script.pyto profile from the command line. - Use
cProfile.run('code')to profile code in your program. tottimeis time inside the function;cumtimeincludes subcalls.- Use
pstats.Statsto sort and filter profiling results programmatically. - Optimize functions with high
tottimefirst, then those with highncalls. - Profile before optimizing; intuition about what is slow is often wrong.
Frequently Asked Questions
What is the difference between cProfile and profile?
How do I profile a Python script from the command line?
What is the difference between tottime and cumtime in profiling output?
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.
More in this topic
Format Dates and Times in Python
Learn how to use strftime and strptime to format dates as strings and parse date strings into Python datetime objects.
What Is the Python Standard Library?
Learn what the Python standard library is, why it matters, and how to use its built-in modules without installing anything extra.
Encode and Decode Base64 Data in Python
Learn how to use Python's base64 module to encode binary data as text and decode Base64 strings back to bytes.