Optimization without measurement often targets the wrong code. Python's built-in cProfile records function calls and timings to show where a workload consumes time.
Define a reproducible question
Profiling is useful when it answers a concrete question such as “why does importing 10,000 rows exceed two seconds?” Record the Python version, dependency versions, input, machine characteristics, and command. Disable unrelated background work and run the workload more than once. A profile from a tiny synthetic input can emphasize startup costs that barely matter in normal use.
python -m cProfile -o profile.prof app.py
python -m pstats profile.prof
Then enter:
sort cumulative
stats 20
tottime is time in a function body excluding subcalls. cumtime includes called functions and highlights expensive paths.
Other columns provide context. ncalls may appear as primitive/total for recursive calls. percall divides time by a call count. A function with modest per-call cost can dominate because it runs millions of times, while one slow function may simply be waiting for an external service.
import cProfile
import pstats
with cProfile.Profile() as profiler:
run_workload()
pstats.Stats(profiler).sort_stats("cumulative").print_stats(20)
Restrict and compare output
print_stats() accepts text and numeric restrictions. Apply them in order to focus on a package or the hottest fraction:
stats = pstats.Stats("profile.prof")
stats.strip_dirs()
stats.sort_stats("cumulative")
stats.print_stats("myapp/", 25)
stats.print_callers(10)
stats.print_callees(10)
strip_dirs() makes paths easier to read but can merge files with identical shortened names. print_callers() shows who invokes a costly function; print_callees() shows where it delegates work. Sorting by tottime exposes expensive function bodies, while cumulative is better for expensive call paths. Inspect both before deciding.
For repeated runs, save separate .prof files and use pstats.Stats to combine statistically comparable executions. Do not add profiles from different workloads and interpret them as one average. For before-and-after work, keep input and environment fixed, compare wall-clock benchmarks as well, and report variability rather than one favorable run.
Profile a command or a narrow phase
The module CLI can profile a script or another module:
python -m cProfile -o import.prof -s cumulative -m myapp.importer sample.csv
Programmatic profiling lets setup remain outside the measured region:
records = load_fixture("sample.json")
profiler = cProfile.Profile()
profiler.enable()
result = transform(records)
profiler.disable()
profiler.dump_stats("transform.prof")
Warm caches only if production normally has warm caches. If cold startup matters, measure it separately. Avoid printing large results inside the region because terminal I/O can dominate.
Interpret CPU and waiting carefully
cProfile is deterministic: it observes Python call events and attributes elapsed time to functions. It adds overhead, especially to code with many tiny calls. Use it to find candidates, then validate improvements with a benchmark that does not run under the profiler.
Elapsed time inside a database client or HTTP function may represent waiting rather than CPU. Optimize the query, batching, network protocol, or concurrency instead of micro-optimizing the Python wrapper. Native extensions may appear as a small number of calls even when substantial work happens below Python. For line-level questions, memory allocation, async task scheduling, or production sampling, choose a tool designed for that dimension.
Follow a disciplined optimization loop
Start with the largest relevant entry that your code can safely change. Form a hypothesis, make one focused change, run correctness tests, benchmark, and profile again. Examples include replacing repeated linear searches with a set, moving invariant parsing outside a loop, batching database access, or removing redundant serialization.
Do not assume fewer calls always means faster. A vectorized library call may do more work in native code but finish sooner. Likewise, memoization can trade memory and staleness for speed. Record both the benefit and the operational cost.
def unique_ids_slow(rows):
result = []
for row in rows:
if row["id"] not in result:
result.append(row["id"])
return result
def unique_ids(rows):
return list(dict.fromkeys(row["id"] for row in rows))
The second version has better lookup behavior while preserving first-seen order, but a benchmark with representative rows must confirm that this function is actually important.
Protect profile data
Profile files can reveal filesystem paths, module names, internal architecture, and function names. Keep them out of public directories and do not commit ad hoc production captures. Profiling live traffic can also expose sensitive context through surrounding logs or fixtures. Use sanitized inputs and an access-controlled storage location.
Use input close to real traffic and distinguish CPU work from network or database waits. A call profiler cannot explain every external bottleneck.
Automated performance checks can catch large regressions, but generous thresholds are safer than brittle timing assertions. Track a benchmark distribution on comparable runners and use profiling only when the benchmark changes.
Avoid common interpretation mistakes
Do not optimize the first row merely because it is first; confirm that its time matters to the user-facing objective. Startup imports may dominate a short command but disappear in a persistent service. Test helpers and fixture creation can dominate a profile if they remain inside the measured region. Recursive functions require reading primitive and total call counts carefully.
Wall-clock improvement is the final criterion for latency work. CPU time, memory use, throughput, and tail latency may move differently, so state which metric is being improved. A change that accelerates the median but makes the slowest requests worse can be a regression for an API.
Keep a small performance notebook with the command, input checksum, environment, baseline, change, result, and decision. This prevents a future maintainer from removing an unusual-looking optimization without knowing its evidence, and it makes rejected experiments useful rather than mysterious.
Optimize the largest safe target, run tests, and measure again. Do not trade maintainability for a small unstable gain. See the Python optimization guide for complementary techniques.
The official Python profiler documentation, accessed July 22, 2026, recommends cProfile for most users because of its lower overhead. Save the workload and measurements so the decision remains reproducible.