Maintenance

Site is under maintenance — quizzes are still available.

Go to quizzes
How-tos

Profiling Python Code with cProfile: Find What's Slow

Learn to use Python's built-in cProfile to identify performance bottlenecks in your code. This practical guide covers running profiles, reading results with pstats, and real-world optimization examples that cut runtime from 47 minutes to 4.

July 2026 5 min read 2 views 0 hearts

You write Python code that works perfectly, but when you run it on real data, it takes forever. We've all been there. The frustrating part is guessing where the bottleneck actually lives.

I spent three hours optimizing a function I was sure was the problem at PythonSkillset, only to discover the real culprit was a tiny loop I had ignored. That's when I learned: don't guess, profile.

What is cProfile?

cProfile is Python's built-in profiler. It records how much time each function in your code spends running, and how often it gets called. No external packages needed — it comes with the standard library.

Think of it like a stopwatch strapped to every single function in your program, logging every call and its duration.

Your First Profile

Let's start with something simple. Imagine you have a script that processes some data:

import cProfile

def slow_function():
    total = 0
    for i in range(1000000):
        total += i ** 2
    return total

def fast_function():
    return sum(i ** 2 for i in range(1000000))

def main():
    a = slow_function()
    b = fast_function()
    print(a, b)

cProfile.run('main()', 'profile_output.txt')

Run this, and cProfile writes a stats file. But raw stats aren't readable. You need pstats to make sense of it.

Reading the Results

import pstats
p = pstats.Stats('profile_output.txt')
p.sort_stats('cumtime').print_stats(10)

This shows you the top 10 functions, sorted by cumulative time (total time spent in that function plus everything it called).

What you'll see:

Function ncalls cumtime percall
main 1 0.245 0.245
slow_function 1 0.218 0.218
fast_function 1 0.027 0.027

Immediately obvious: slow_function takes 0.218 seconds, fast_function takes only 0.027. The fast_function is nearly 10x faster.

Real Scenario at PythonSkillset

At PythonSkillset, we had a content processing pipeline that took 47 minutes to run on a batch of articles. We had added caching, used comprehensions, even tried multiprocessing — but nothing helped much.

Then we profiled it.

The profiler showed that 78% of time was spent in a function called clean_unicode. It was being called 14,000 times per article — not once, as the developer assumed.

# Before - called 14,000 times per article
def process_article(text):
    paragraphs = text.split('\n')
    clean_paragraphs = []
    for p in paragraphs:
        # This function call was repeated inside a loop
        clean_paragraphs.append(clean_unicode(p))
    return '\n'.join(clean_paragraphs)

# After - call once
def process_article(text):
    text = clean_unicode(text)  # one call for entire text
    return text

That single fix cut the pipeline from 47 minutes to 4 minutes. Profiling made the difference.

Profiling in the Real World

Three patterns I see everywhere that profiling catches:

1. Hidden loops inside library calls You call str.replace() inside a loop? That's fine. But if you call re.sub() inside a loop processing 100,000 items, the regex engine overhead kills performance.

2. Too many attribute lookups

# Slow
for item in big_list:
    if item.property == 'x':
        process(item.property)

# Faster - cache the attribute
prop = item.property
if prop == 'x':
    process(prop)

3. Overzealous logging We had a developer at PythonSkillset who put a debug logging call inside a deep loop. In production, that logging call was formatted and discarded because the log level was set to WARNING. But it still built the string. Profiling caught it immediately.

Practical Tips for Profiling

Profile on realistic data, not test data. Profiling a function on 100 items vs 100,000 items shows completely different bottlenecks.

Don't profile the first run. Python caches bytecode and warms up caches. Run your script twice — discard the first, profile the second.

Use context managers for targeted profiling.

import cProfile

with cProfile.Profile() as profile:
    my_function(real_data)

profile.dump_stats('targeted_profile.txt')

This only profiles my_function, not the entire module load.

What Not to Over-Optimize

Profiling shows you the top bottlenecks. Usually the top 3 functions account for 80% of the runtime. Fix those. The bottom 20 functions might account for 2% total — optimizing them is a waste of brain space.

At PythonSkillset, I once spent a day micro-optimizing a sorting routine that saved 0.003 seconds. Profiling later showed I could have saved 3 seconds by changing one database query. Don't be me.

Running cProfile from the Command Line

Sometimes you want to profile an entire script without editing code:

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

Then read it with:

python -c "import pstats; pstats.Stats('output.prof').sort_stats('cumtime').print_stats(20)"

Wrapping Up

cProfile has been in Python since version 2.5. It's free, it's built-in, and it will save you hours of guessing. Every time you find yourself thinking "this should be faster but I don't know why" — run a profile. The answer is usually right there in the numbers.

The next time you're stuck with a slow script at PythonSkillset, skip the random optimizations. Run cProfile, read the output, and fix what actually matters. Your future self will thank you.

Comments

Questions, corrections, and tips stay visible for everyone reading this page.

0 in thread

Join the discussion

Shown next to your comment.

Up to 4,000 characters

No comments yet

Be the first to leave a note — it helps the next reader.