Introduction
cProfile is Python's built-in deterministic profiler. It traces every function call and return, recording how many times each function was called and how long each call took. It is the standard first tool for identifying performance bottlenecks in Python applications.
Key Concepts
ncalls: The number of times a function was called. A slash (e.g.,100/5) means 100 total calls, 5 non-recursive.tottime: Total time spent inside the function itself, excluding time in subcalls. This answers "how much work does this function do directly?"cumtime: Cumulative time spent in the function including all subcalls. This answers "how much wall time does calling this function cost?"percall: Time per call —tottime/ncallsorcumtime/ncallsdepending on the column.pstats: The standard library module for loading, filtering, and sorting cProfile output files.
Real World Context
When a Django request takes 2 seconds but you do not know whether the database query, serialization, or middleware is the bottleneck, running cProfile on the request handler immediately reveals which functions consume the most time. In data pipelines, cProfile identifies whether data loading, transformation, or I/O is the dominant cost.
Deep Dive
Command Line Usage
The simplest way to profile a script:
bashpython -m cProfile script.py
This prints all function calls sorted by the default order (filename/line). To sort by most useful metrics:
bash# Sort by total time in function (excluding subcalls) python -m cProfile -s tottime script.py # Sort by cumulative time (including subcalls) python -m cProfile -s cumtime script.py # Save to file for later analysis python -m cProfile -o profile.prof script.py
Understanding the Output
Here is a typical cProfile output:
1000003 function calls in 0.842 seconds
Ordered by: internal time
ncalls tottime percall cumtime percall filename:lineno(function)
1000000 0.563 0.000 0.563 0.000 example.py:10(process_item)
1 0.205 0.205 0.842 0.842 example.py:5(main)
1 0.074 0.074 0.637 0.637 example.py:15(process_all)
1 0.000 0.000 0.842 0.842 {built-in method builtins.exec}
Reading this: process_item was called 1,000,000 times, spending 0.563 seconds total in the function itself. main spent 0.205 seconds in its own code but 0.842 seconds total (including subcalls).
Programmatic Usage
For profiling specific sections of code:
pythonimport cProfile import pstats profiler = cProfile.Profile() profiler.enable() # Code to profile result = process_data(large_dataset) profiler.disable() # Print sorted stats stats = pstats.Stats(profiler) stats.sort_stats('tottime') stats.print_stats(20) # Top 20 functions
Filtering with pstats
When the output is overwhelming, pstats lets you focus on what matters:
pythonimport pstats # Load saved profile stats = pstats.Stats('profile.prof') # Show only functions in your own code stats.sort_stats('cumtime') stats.print_stats('myproject/') # Filter by path substring # Show callers of a specific function stats.print_callers('process_item') # Show what a function calls stats.print_callees('main')
Using as a Context Manager
For clean scoping:
pythonimport cProfile with cProfile.Profile() as profiler: result = expensive_function() profiler.print_stats('tottime')
Common Pitfalls
- Profiling too much code at once: cProfile output for a full application run can have thousands of entries. Always narrow down to the specific request or operation you want to optimize.
- Confusing tottime and cumtime: If
cumtimeis high buttottimeis low, the function itself is fast — it is spending time waiting on the functions it calls. Optimize those subcalls instead. - Not accounting for profiler overhead: cProfile adds 10-30% overhead because it hooks into every function call and return. Tight loops with millions of tiny function calls will appear slower than they actually are.
Best Practices
- Sort by
tottimefirst to find functions doing the most direct work, then bycumtimeto understand the call chain. - Use
print_callers()andprint_callees()to trace the call graph when you find a hot function. - Save profiles to files (
-o profile.prof) so you can re-analyze without re-running the code.
Summary
python -m cProfile -s tottime script.pyis the fastest way to find which functions consume the most direct CPU time.tottimemeasures time in the function only;cumtimeincludes all subcall time.- Use
pstats.Statsto filter, sort, and explore profile data programmatically. print_callers()andprint_callees()trace the call graph around hot functions.- cProfile adds measurable overhead — keep this in mind when interpreting results for call-heavy code.
Code Examples
import cProfile
import pstats
import io
def slow_function():
total = 0
for i in range(500_000):
total += i ** 2
return total
def fast_function():
return sum(i ** 2 for i in range(500_000))
def main():
slow_function()
fast_function()
# Profile and print top functions sorted by tottime
profiler = cProfile.Profile()
profiler.enable()
main()
profiler.disable()
stream = io.StringIO()
stats = pstats.Stats(profiler, stream=stream)
stats.sort_stats('tottime')
stats.print_stats(10)
print(stream.getvalue())import cProfile
import pstats
# Profile and save to file
cProfile.run('import json; json.dumps(list(range(100_000)))', 'json_profile.prof')
# Analyze the saved profile
stats = pstats.Stats('json_profile.prof')
stats.sort_stats('cumtime')
stats.print_stats(10)
# Trace call relationships
print("\n--- Callees of dumps ---")
stats.print_callees('dumps')