Python Profiling: cProfile, py-spy, and Scalene for Performance
Master Python profiling with cProfile, py-spy, and Scalene.
20+ years shipping production Python across data and backend systems. Notes here come from systems that actually shipped.
- ✓Basic Python knowledge (functions, loops, imports)
- ✓Familiarity with command line and running Python scripts
- ✓Understanding of CPU and memory concepts
- Profiling measures where your Python code spends time and memory.
- cProfile is built-in, deterministic, but adds overhead.
- py-spy is a sampling profiler that works in production with minimal overhead.
- Scalene is a high-performance profiler that tracks CPU, memory, and GPU usage line by line.
- Choose based on need: cProfile for detailed call counts, py-spy for live production, Scalene for comprehensive analysis.
Imagine your code is a factory. Profiling is like installing sensors on each machine to see which one is slow or using too much power. cProfile is a detailed stopwatch for each task, py-spy is a camera that takes snapshots of what's running, and Scalene is a smart meter that shows energy (CPU) and material (memory) usage per machine.
| Chrome | Firefox | Safari | Edge |
|---|---|---|---|
| ✓ | ✓ | ✓ | ✓ |
You've written a Python script that works perfectly on your laptop, but in production it crawls. Or maybe your web API responds in milliseconds during testing but takes seconds under load. The culprit isn't always obvious—it could be a hidden loop, an inefficient data structure, or a memory leak. Without data, you're guessing. Profiling gives you hard numbers.
Python offers several profiling tools, each with strengths. The built-in cProfile module provides deterministic profiling—it records every function call and its duration. It's great for development but adds overhead that can distort results. py-spy is a sampling profiler that attaches to a running Python process without modifying code, making it ideal for production debugging. Scalene is a modern profiler that tracks CPU, memory, and GPU usage line by line with minimal overhead, offering a comprehensive view.
In this tutorial, you'll learn how to use each tool, interpret their output, and apply them to real-world scenarios. We'll walk through code examples, a production incident, and debugging guides to turn you into a profiling pro. By the end, you'll know which tool to reach for when your Python code needs a speed boost.
Getting Started with cProfile
cProfile is Python's built-in deterministic profiler. It records every function call, including the number of calls, total time, and cumulative time. Use it when you need detailed call counts and you're profiling in a development or staging environment.
To profile a script, run:
``bash python -m cProfile -o output.prof my_script.py ``
The -o flag saves the profile data to a file. You can then analyze it with the pstats module or visualize with tools like snakeviz.
Let's profile a simple script that simulates a slow computation.
Analyzing cProfile Output with pstats
The pstats module allows you to sort and filter profile data. After generating a .prof file, you can start an interactive session:
``python import pstats p = pstats.Stats('output.prof') p.sort_stats('cumtime').print_stats(10) ``
This prints the top 10 functions by cumulative time. You can also sort by 'time' (total time spent in the function itself) or 'calls' (number of calls).
Let's analyze the profile from our slow script.
Sampling Profiling with py-spy
py-spy is a sampling profiler that attaches to a running Python process without modifying its code. It takes snapshots of the call stack at regular intervals (e.g., every 10ms). This makes it safe for production use with minimal overhead (typically <1% CPU).
Install py-spy: pip install py-spy
To profile a running script, first get its PID:
``bash py-spy record -o flamegraph.svg --pid 12345 ``
This generates a flame graph SVG that you can open in a browser. You can also use py-spy top for a live top-like view.
Let's simulate a production-like scenario: a Flask app with a slow endpoint.
top command but for Python call stacks.Interpreting py-spy Flame Graphs
Flame graphs are a visualization of stack traces. Each rectangle represents a function call, with width proportional to the time spent in that function (including its children). The x-axis spans the sample population, and the y-axis shows the call stack depth.
To generate a flame graph with py-spy:
``bash py-spy record -o flame.svg --duration 30 --pid 12345 ``
Open flame.svg in a browser. Look for wide rectangles at the top—they indicate functions where the program spends most of its time. The color is usually random but helps distinguish different stacks.
In our Flask example, you'd see slow (the view function) wide, with time.sleep and the loop inside it. If the loop is the bottleneck, it will appear as a wide rectangle.
Comprehensive Profiling with Scalene
Scalene is a high-performance CPU, memory, and GPU profiler for Python. It uses sampling and special techniques to achieve low overhead (typically 10-20% slower) while providing line-by-line metrics. It can also detect memory leaks and copy volume.
Install Scalene: pip install scalene
To profile a script:
``bash scalene --html --outfile profile.html my_script.py ``
This generates an HTML report with color-coded lines: red for CPU-intensive, blue for memory-intensive.
Let's profile a script with both CPU and memory issues.
Interpreting Scalene Reports
Scalene's HTML report shows your source code with annotations. Each line has a bar indicating CPU time (red) and memory usage (blue). The width of the bar is proportional to the metric. Hovering over a line shows exact numbers.
For memory, Scalene distinguishes between memory allocated and memory copied. High copy volume often indicates inefficient use of data structures (e.g., copying large lists).
Let's look at a sample report for our memory_cpu_script.
Choosing the Right Profiler
Each profiler has its sweet spot. Here's a quick decision guide:
- cProfile: Use in development when you need exact call counts and detailed function-level timing. Avoid in production due to overhead.
- py-spy: Use in production for live debugging of CPU-bound issues. Minimal overhead, no code changes. Best for flame graphs.
- Scalene: Use during optimization when you need both CPU and memory insights. Ideal for data-intensive applications.
Consider combining them: use py-spy to identify the hot function in production, then use Scalene locally to drill into that function's memory behavior.
Let's see a scenario where we profile a data processing pipeline.
Best Practices for Profiling
To get reliable results, follow these guidelines:
- Profile realistic workloads: Use data sizes and concurrency levels similar to production.
- Warm up: Run your code a few times before profiling to avoid including startup overhead.
- Profile multiple times: Variability is normal; take the median or average.
- Focus on bottlenecks: Optimize the top 10% of time-consuming functions first.
- Measure after optimization: Always re-profile to confirm improvement.
Let's implement a simple benchmarking decorator that uses cProfile conditionally.
The Slow API: A Production Profiling Story
- Don't assume the bottleneck is I/O; CPU-bound code can be the culprit.
- Use profiling before optimizing—cProfile or py-spy can pinpoint the exact function.
- Sampling profilers like py-spy are safe to use in production with minimal overhead.
- Line-by-line profilers (like Scalene) help identify specific lines causing slowness.
- Always measure after a fix to confirm improvement.
py-spy record -o profile.svg --pid <PID> then view flamegraph.scalene --memory your_script.pypython -m cProfile -o output.prof your_script.py then analyze with pstats or snakeviz.py-spy top --pid <PID>python -m cProfile -o out.prof script.pypython -m pstats out.prof| File | Command / Code | Purpose |
|---|---|---|
| slow_script.py | def slow_function(): | Getting Started with cProfile |
| analyze_profile.py | p = pstats.Stats('output.prof') | Analyzing cProfile Output with pstats |
| app.py | from flask import Flask | Sampling Profiling with py-spy |
| generate_flame.sh | py-spy record -o flame.svg --duration 10 --pid $(pgrep -f app.py) | Interpreting py-spy Flame Graphs |
| memory_cpu_script.py | def compute(): | Comprehensive Profiling with Scalene |
| scalene_report.html | Interpreting Scalene Reports | |
| pipeline.py | def load_data(): | Choosing the Right Profiler |
| profile_decorator.py | def profile(output_file=None): | Best Practices for Profiling |
Key takeaways
Interview Questions on This Topic
Explain the difference between cProfile and py-spy.
Frequently Asked Questions
20+ years shipping production Python across data and backend systems. Notes here come from systems that actually shipped.
That's Advanced Python. Mark it forged?
3 min read · try the examples if you haven't