Profiling Python Code With Py-Spy Without Changing a Line
Learn how to use py-spy, a sampling profiler that attaches to running Python processes from outside, to diagnose CPU bottlenecks in production or local scripts without modifying code.
Here's the article:
Profiling Python Code With Py-Spy: When You Need Answers Without Changing a Single Line
You know that feeling when your Python script runs slower than a wet weekend? You've tried adding print statements. You've stared at the code. But the bottleneck stays hidden. That's where py-spy comes in — and trust me, it's different from every other profiler you've used.
Why Py-Spy Breaks the Rules
Most profilers require you to modify your code. You wrap functions with decorators. You import profiling modules. You change how your program runs. That's fine for your own scripts, but what about production code? What about a third-party library you can't touch?
Py-spy doesn't care about any of that. It's a sampling profiler that attaches to a running Python process from the outside. Think of it like a mechanic who can diagnose your engine without opening the hood. It reads the call stack at regular intervals and builds a picture of where your CPU time actually goes.
Here at PythonSkillset, we've used py-spy to debug Celery workers that were mysteriously slow, Django views that timed out, and even Jupyter notebooks that froze mid-analysis. Every time, we got answers in minutes.
Installing Py-Spy
The installation is straightforward via pip:
pip install py-spy
But here's the thing — it works best with Python 3.6 and above. If you're still on Python 2.7, I'm sorry, but it's time to upgrade.
The Basic Usage: Attaching to a Running Process
Imagine you have a Python script that's chugging along right now. You know its process ID ( PID ). On Linux or macOS, you can find it with ps aux | grep python.
Then just run:
py-spy record -o profile.svg --pid 12345
Replace 12345 with your actual PID. Py-spy will sample the call stack every 100 milliseconds by default and generate a flame graph as an SVG file. Open that SVG in your browser, and you'll see exactly which functions are hogging the CPU.
Here's a real example from PythonSkillset. We had a data processing pipeline that took 45 minutes. Running py-spy for 30 seconds showed us that 80% of the time was spent in a pandas.apply() call that could be replaced with a vectorized operation. After the fix, the pipeline ran in under 5 minutes.
Profiling a Python Script on the Fly
What if your script finishes before you can attach the profiler? You can run py-spy alongside it:
py-spy record -o profile.svg -- python my_slow_script.py
This launches the script and starts profiling immediately. When the script exits, py-spy saves the flame graph. No code changes needed.
Using Py-Spy in Production Without Fear
This is where py-spy shines. Because it only reads memory — it never writes — it's safe to use on production systems. The overhead is minimal, around 1-2% CPU in our tests at PythonSkillset.
But a word of caution: on Linux, you might need sudo or set the ptrace scope:
sudo py-spy record -o profile.svg --pid 12345
Or configure your system to allow non-root users to trace processes:
echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
Reading the Flame Graph
The flame graph might look intimidating, but it's simple. The bottom bar is the main function. Every bar stacked above is a child function. The wider the bar, the more time spent in that function.
Look for wide bars at the top — those are your hot spots. In our experience, people often guess wrong about what's slow. Py-spy removes the guesswork.
Real-World Debugging: A Django View Example
At PythonSkillset, we once had a Django view that took 12 seconds to load. We ran:
py-spy record -o django_profile.svg --pid $(pgrep -f gunicorn)
The flame graph revealed that django-debug-toolbar's SQL panel was running 400+ queries in a single render. The toolbar was supposed to be disabled in production, but someone had forgotten to set DEBUG=False. One line fix, and the view returned to 200 milliseconds.
When Py-Spy Won't Help
Py-spy is a sampling profiler for CPU-bound tasks. If your code is waiting on I/O — like network requests or disk reads — py-spy will show those functions as using CPU time even though they're mostly idle. For I/O profiling, you'd need a tracing profiler like cProfile.
Also, py-spy doesn't work on Windows yet. The developers are working on it, but for now, Windows users need to use WSL or another tool.
Tips From PythonSkillset's Trenches
- Profile for at least 10 seconds — shorter runs miss intermittent issues.
- Use the
--durationflag —py-spy record --duration 60captures exactly one minute. - Generate a speedscope file — use
-o profile.speedscope.jsonfor a more interactive view. - Check with
--nonblocking— if you getPermission denied, that's your fix.
The Bottom Line
Py-spy is the Swiss Army knife for Python performance debugging. It works where other profilers can't — on production, on locked-down systems, on third-party code. It gives you facts, not guesses. And it does it without changing a single line of your application.
Next time a Python script drags, don't sprinkle print statements everywhere. Install py-spy, run it for 30 seconds, and watch the flame graph tell you the truth.
Comments
Questions, corrections, and tips stay visible for everyone reading this page.
Join the discussion
No comments yet
Be the first to leave a note — it helps the next reader.