Skip to Content
Course content

356: Profiling Code with cProfile

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Imagine you're running a professional kitchen during a dinner rush. The food is getting to the tables too slowly, and the customers are getting grumpy. You know the kitchen is the bottleneck, but you don't know why. Do you just tell every single chef to "work faster"? Of course not. That's a waste of energy. Instead, you stand back with a stopwatch and observe. You notice the grill chef is lightning fast, but the prep cook is spending ten minutes chopping parsley for every single plate. You've found the bottleneck. Now you can actually fix the problem—maybe by prepping the parsley in advance—without bothering the grill chef.

In Python, cProfile is that stopwatch. When your code feels sluggish, the biggest mistake I see developers make is "guessing" where the lag is. They'll rewrite a function that looks complex but only takes 2% of the total execution time, while the real culprit—maybe a hidden nested loop or a slow API call—continues to eat up 80% of the runtime. We call this premature optimization, and it's a great way to waste a Tuesday afternoon.

Stop Guessing Where the Lag Is

Let's look at a real scenario. Say you've written a script to filter a large list of user IDs against a "blacklist." It works, but it's taking forever as the list grows.

import cProfile

def check_blacklist(user_ids, blacklist):
    valid_users = []
    for user in user_ids:
        if user not in blacklist:  # This is the hidden killer
            valid_users.append(user)
    return valid_users

def run_process():
    # Simulate 10,000 users and a blacklist of 5,000
    users = list(range(10000))
    blacklist = list(range(5000))
    check_blacklist(users, blacklist)

# Instead of just calling run_process(), we wrap it in cProfile
cProfile.run('run_process()')

If you run this, Python won't just execute the code; it'll spit out a table. At first glance, the table looks like a wall of text, but I only ever care about a few specific columns.

Reading the Profiler's Output

When you see the results, you'll notice columns like ncalls, tottime, and cumtime. Here is how I translate those into English:

  • ncalls: How many times the function was called. If this number is in the millions and you didn't expect it to be, you've probably got a loop problem.
  • tottime: The total time spent inside this function, excluding time spent in other functions it called. This is the "holy grail" for finding bottlenecks.
  • cumtime: The total time spent in this function including all the sub-functions it called. This tells you which high-level process is slow, but not necessarily which specific line of code is the culprit.

In our blacklist example, you'll see that check_blacklist has a massive tottime. Why? Because checking if user not in blacklist on a list takes O(n) time. Python has to scan the entire list over and over again. I've spent way too many hours of my life fixing "slow code" only to realize I was just using a list where I should have used a set.

Turning Data into Speed

Once cProfile points the finger at check_blacklist, the fix becomes obvious. I change the blacklist from a list to a set. In Python, set lookups are O(1)—they're nearly instantaneous regardless of the size of the set.

def check_blacklist_fast(user_ids, blacklist_set):
    # Converting to a set once is much cheaper than 
    # scanning a list 10,000 times.
    return [user for user in user_ids if user not in blacklist_set]

If you profile the updated version, you'll see the tottime for that function drop from seconds to milliseconds. That's the power of profiling: you stop guessing and start measuring.




📋 Practical Task

Profiling and Optimizing the Log Analyzer

You have been given a piece of code that analyzes system logs to find unique error messages. However, as the log file grows, the script becomes unusable. Your goal is to use cProfile to identify the exact function causing the slowdown and then optimize it.

The Code:

import cProfile

def find_unique_errors(logs):
    unique_errors = []
    for log in logs:
        if "ERROR" in log:
            # We only want to keep the error message if we haven't seen it yet
            if log not in unique_errors:
                unique_errors.append(log)
    return unique_errors

def simulate_logs():
    # Generating 20,000 log entries with many duplicates
    base_errors = ["ERROR: Connection Timeout", "ERROR: Disk Full", "ERROR: Auth Failed"]
    logs = [base_errors[i % 3] + f" - line {i}" for i in range(20000)]
    # To make it really slow, let's add some actual duplicates
    logs = logs * 2 
    return logs

def main():
    logs = simulate_logs()
    find_unique_errors(logs)

cProfile.run('main()')

Your Task:

  1. Run the code and analyze the cProfile output. Which function is consuming the most tottime?
  2. Identify the line inside that function causing the bottleneck.
  3. Rewrite the find_unique_errors function using a more efficient data structure to track seen errors.
  4. Run cProfile again to verify that the tottime for that function has significantly decreased.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.