Home Python Python Profiling: cProfile, py-spy, and Scalene for Performance
Advanced 3 min · July 14, 2026

Python Profiling: cProfile, py-spy, and Scalene for Performance

Master Python profiling with cProfile, py-spy, and Scalene.

N
Naren Founder & Principal Engineer

20+ years shipping production Python across data and backend systems. Notes here come from systems that actually shipped.

Follow
Production
production tested
July 15, 2026
last updated
2,398
articles · all by Naren
Before you start⏱ 20-25 min read
  • Basic Python knowledge (functions, loops, imports)
  • Familiarity with command line and running Python scripts
  • Understanding of CPU and memory concepts
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • 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.
✦ Definition~90s read
What is Python Profiling?

Profiling is the process of measuring where your Python code spends time and memory to identify performance bottlenecks.

Imagine your code is a factory.
Plain-English First

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.

⚙ Browser compatibility
Latest versions — ✓ supported
ChromeFirefoxSafariEdge

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.

``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.

slow_script.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import time

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

def main():
    result = slow_function()
    print(f"Result: {result}")

if __name__ == "__main__":
    main()
Output
Result: 333332833333500000
💡Use snakeviz for visualization
📊 Production Insight
In production, avoid cProfile because it modifies the code execution and adds significant overhead. Use sampling profilers instead.
🎯 Key Takeaway
cProfile gives you exact call counts and times, but its overhead can slow down your program by 10-100x.

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.

analyze_profile.pyPYTHON
1
2
3
4
import pstats

p = pstats.Stats('output.prof')
p.sort_stats('cumtime').print_stats(10)
Output
1000004 function calls in 0.123 seconds
Ordered by: cumulative time
List reduced from 4 to 10 due to restriction <10>
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.000 0.000 0.123 0.123 slow_script.py:1(<module>)
1 0.000 0.000 0.123 0.123 slow_script.py:7(main)
1 0.123 0.123 0.123 0.123 slow_script.py:2(slow_function)
1000001 0.000 0.000 0.000 0.000 {built-in method builtins.print}
1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}
🔥Understanding columns
🎯 Key Takeaway
pstats helps you drill down into the most time-consuming functions quickly.

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

``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.

app.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from flask import Flask
import time

app = Flask(__name__)

@app.route('/slow')
def slow():
    time.sleep(0.5)  # Simulate I/O wait
    total = 0
    for i in range(1000000):
        total += i ** 2
    return str(total)

if __name__ == '__main__':
    app.run()
⚠ py-spy requires appropriate permissions
📊 Production Insight
Use py-spy top to monitor a live process in real-time, similar to the Unix top command but for Python call stacks.
🎯 Key Takeaway
py-spy is ideal for profiling in production because it doesn't require code changes and has negligible overhead.

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.

``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.

generate_flame.shBASH
1
py-spy record -o flame.svg --duration 10 --pid $(pgrep -f app.py)
💡Compare before and after
📊 Production Insight
In production, run py-spy for a short duration (e.g., 30 seconds) to minimize overhead while capturing representative samples.
🎯 Key Takeaway
Flame graphs give you a visual, intuitive understanding of where time is spent.

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

``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.

memory_cpu_script.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
import numpy as np

def compute():
    # Memory-intensive: allocate large array
    data = np.random.rand(10000, 10000)
    # CPU-intensive: perform computation
    result = np.sum(data ** 2)
    return result

if __name__ == '__main__':
    print(compute())
🔥Scalene's memory profiling
📊 Production Insight
Scalene's overhead is higher than py-spy but lower than cProfile. Use it in staging or during load testing, not in production under heavy load.
🎯 Key Takeaway
Scalene provides a holistic view of performance, combining CPU, memory, and GPU metrics in one tool.

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.

scalene_report.htmlHTML
1
2
3
4
5
6
7
<!DOCTYPE html>
<html>
<head><title>Scalene Report</title></head>
<body>
<!-- Scalene generates an interactive HTML file -->
</body>
</html>
Try it live
💡Use Scalene's --cli flag for terminal output
📊 Production Insight
Scalene can also profile GPU usage if you have CUDA installed, making it useful for data science workloads.
🎯 Key Takeaway
Scalene's line-by-line annotations make it easy to pinpoint exactly which lines are causing performance issues.

Choosing the Right Profiler

  • 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.

pipeline.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import pandas as pd
import numpy as np

def load_data():
    return pd.DataFrame(np.random.rand(100000, 50))

def process(df):
    # Expensive operation
    return df.apply(lambda x: x ** 2)

if __name__ == '__main__':
    df = load_data()
    result = process(df)
    print(result.shape)
Output
(100000, 50)
⚠ Profiling pandas code
📊 Production Insight
In production, start with py-spy to get a quick overview. If memory is a concern, use Scalene in a staging environment with realistic data.
🎯 Key Takeaway
Match the profiler to your environment and the type of bottleneck you suspect.

Best Practices for Profiling

  1. Profile realistic workloads: Use data sizes and concurrency levels similar to production.
  2. Warm up: Run your code a few times before profiling to avoid including startup overhead.
  3. Profile multiple times: Variability is normal; take the median or average.
  4. Focus on bottlenecks: Optimize the top 10% of time-consuming functions first.
  5. Measure after optimization: Always re-profile to confirm improvement.

Let's implement a simple benchmarking decorator that uses cProfile conditionally.

profile_decorator.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import cProfile
import functools

def profile(output_file=None):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            profiler = cProfile.Profile()
            profiler.enable()
            result = func(*args, **kwargs)
            profiler.disable()
            if output_file:
                profiler.dump_stats(output_file)
            else:
                profiler.print_stats()
            return result
        return wrapper
    return decorator

@profile(output_file='my_func.prof')
def my_func():
    total = 0
    for i in range(1000000):
        total += i ** 2
    return total

if __name__ == '__main__':
    my_func()
💡Use environment variables to toggle profiling
📊 Production Insight
Never leave profiling code in production. Use feature flags or environment variables to enable it only during debugging sessions.
🎯 Key Takeaway
Integrate profiling into your development workflow with decorators or context managers for easy toggling.
● Production incidentPOST-MORTEMseverity: high

The Slow API: A Production Profiling Story

Symptom
API endpoint /search took 2+ seconds to respond when handling 100 concurrent requests.
Assumption
The developer assumed the bottleneck was the database query, so they optimized indexes and added caching.
Root cause
A nested loop in Python that processed search results was O(n^2) with n=5000, causing 25 million iterations per request.
Fix
Replaced the nested loop with a dictionary lookup, reducing time from 2 seconds to 50ms.
Key lesson
  • 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.
Production debug guideSymptom to Action4 entries
Symptom · 01
API endpoint slow under load
Fix
Attach py-spy to the process: py-spy record -o profile.svg --pid <PID> then view flamegraph.
Symptom · 02
Memory usage grows over time
Fix
Use Scalene with memory profiling: scalene --memory your_script.py
Symptom · 03
Function takes too long in development
Fix
Run cProfile: python -m cProfile -o output.prof your_script.py then analyze with pstats or snakeviz.
Symptom · 04
CPU spikes intermittently
Fix
Use py-spy top to see live function call stacks: py-spy top --pid <PID>
★ Quick Debug Cheat SheetImmediate actions for common performance symptoms.
Slow function call
Immediate action
Profile with cProfile
Commands
python -m cProfile -o out.prof script.py
python -m pstats out.prof
Fix now
Optimize the top time-consuming function.
Production slowdown+
Immediate action
Attach py-spy
Commands
py-spy record -o flame.svg --pid $(pgrep -f script.py)
py-spy top --pid $(pgrep -f script.py)
Fix now
Identify hot path and refactor.
Memory leak+
Immediate action
Run Scalene with memory
Commands
scalene --memory --outfile scalene.html script.py
open scalene.html
Fix now
Free unused objects or use weak references.
FeaturecProfilepy-spyScalene
TypeDeterministicSamplingSampling + instrumentation
OverheadHigh (10-100x)Low (<1%)Moderate (10-20%)
Production safeNoYesNo (use in staging)
Memory profilingNoNoYes
GPU profilingNoNoYes
Line-level detailNoNoYes
VisualizationsnakevizFlame graphHTML report
⚙ Quick Reference
8 commands from this guide
FileCommand / CodePurpose
slow_script.pydef slow_function():Getting Started with cProfile
analyze_profile.pyp = pstats.Stats('output.prof')Analyzing cProfile Output with pstats
app.pyfrom flask import FlaskSampling Profiling with py-spy
generate_flame.shpy-spy record -o flame.svg --duration 10 --pid $(pgrep -f app.py)Interpreting py-spy Flame Graphs
memory_cpu_script.pydef compute():Comprehensive Profiling with Scalene
scalene_report.htmlInterpreting Scalene Reports
pipeline.pydef load_data():Choosing the Right Profiler
profile_decorator.pydef profile(output_file=None):Best Practices for Profiling

Key takeaways

1
Use cProfile for detailed development profiling, py-spy for production debugging, and Scalene for comprehensive CPU+memory analysis.
2
Flame graphs from py-spy provide an intuitive visual representation of where time is spent.
3
Scalene's line-by-line annotations help pinpoint exact lines causing CPU or memory issues.
4
Always profile realistic workloads and measure after optimization to confirm improvements.
5
Integrate profiling into your workflow with decorators or environment variables for easy toggling.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Explain the difference between cProfile and py-spy.
Q02SENIOR
How would you debug a memory leak in a Python application?
Q03JUNIOR
What is a flame graph and how do you interpret it?
Q01 of 03SENIOR

Explain the difference between cProfile and py-spy.

ANSWER
cProfile is a deterministic profiler that records every function call, providing exact call counts and times but adding significant overhead. py-spy is a sampling profiler that takes periodic snapshots of the call stack, offering statistical estimates with minimal overhead, making it suitable for production.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
What is the difference between deterministic and sampling profiling?
02
Can I use py-spy on a Docker container?
03
How does Scalene track memory usage?
04
Which profiler should I use for a web application?
05
Can profiling slow down my application?
N
Naren Founder & Principal Engineer

20+ years shipping production Python across data and backend systems. Notes here come from systems that actually shipped.

Follow
Verified
production tested
July 15, 2026
last updated
2,398
articles · all by Naren
🔥

That's Advanced Python. Mark it forged?

3 min read · try the examples if you haven't

Previous
Python Logging: Production Patterns and Best Practices
27 / 35 · Advanced Python
Next
CI/CD for Python: GitHub Actions, Testing, and Deployment