py-spy: Sampling Profiling for Running Python Processes
Sampling profiler for Python programs
At a glance
- What is it?
- py-spy reads a live CPython process from the outside, so you can profile production code without editing it. Here is how the sampling works, how to install it, and where it stops being the right tool.
- Who is it for?
- Adopt py-spy when you need a profile of a process you cannot restart or edit, especially on a production host where the code is already running and you only have a PID. Do not adopt it as a replacement for a deterministic line-level profiler during development, and do not expect it to explain native allocation or memory growth, which it does not measure.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 47 days ago.
- What is it written in?
- Mainly Rust, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 29, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The problem py-spy solves: profiling code you cannot restart
Most Python profilers run inside the process they measure. They wrap the interpreter, install hooks, or import a module before the work begins. That design forces a choice: either you accept the slowdown on a live service, or you reproduce the problem somewhere else and hope it still appears. py-spy takes the other position. The README states that it lets you "visualize what your Python program is spending time on without restarting the program or modifying the code in any way." The target process is not asked to cooperate.
The audience follows from that constraint. Someone debugging a latency spike on a production box, a support engineer who has a PID and no shell history, a developer profiling a subprocess spawned deep inside a test suite. All of them have a running interpreter and no clean way to instrument it. py-spy is written in Rust and does not run in the same process as the profiled program, which the README gives as the reason it is "safe to use against production Python code." That is a claim about the mechanism, not a guarantee: the profiler still has to read memory from another process, and that access is what determines whether it works at all.
How py-spy reads a Python call stack from outside
The mechanism is direct memory reading. On Linux py-spy uses the process_vm_readv system call, on macOS the vm_read call, and on Windows ReadProcessMemory. Nothing is injected into the target and no bytecode is patched.
Reconstructing a stack means finding the interpreter first. py-spy reads the global PyInterpreterState, walks the Python threads registered there, and iterates the PyFrameObject chain in each thread to produce a call stack. Because the Python ABI shifts between releases, the project uses Rust's bindgen to generate separate structures per interpreter version, and those generated layouts are what the profiler uses to interpret the memory it read. The README lists support for CPython 2.3 through 2.7 and 3.3 through 3.14.
Locating the interpreter is the part that fails in the field. With Address Space Layout Randomization, py-spy dereferences the interp_head or _PyRuntime variable when the interpreter ships with symbols. When binaries are stripped, or Windows builds ship without PDB files, the README says py-spy scans the BSS section for addresses that look like a valid PyInterpreterState and validates the layout it finds. That fallback is a heuristic, and it is the reason a profile can come back empty or wrong on an unusual build.
Installing py-spy and recording a first flame graph
The shortest path is a prebuilt binary wheel from PyPI. The README gives this as the primary install method:
pip install py-spyThere are other routes. Rust users can build from source with cargo install py-spy, which the README notes requires libunwind on Linux and Windows, for example apt install libunwind-dev. On macOS, brew install py-spy pulls the Homebrew formula. On Arch Linux the AUR package installs with yay -S py-spy, and on Alpine the testing repository provides it via apk add py-spy with the repository flag and --allow-untrusted.
The first real use is recording to a flame graph. You can target a PID or launch the program under the profiler:
py-spy record -o profile.svg --pid 12345
# OR
py-spy record -o profile.svg -- python myprogram.pyThe output is an interactive SVG flame graph. The --format flag switches to speedscope profiles or raw data instead. The README points at py-spy record --help for the rest of the options, which include changing the sampling rate, filtering to threads that hold the GIL, profiling native C extensions, showing thread ids, and profiling subprocesses.
For a live view rather than a file, top behaves like the Unix top command for Python functions:
py-spy top --pid 12345And when you only want one snapshot of where a process is stuck, dump prints the current call stack for each thread, with --locals available to include local variables in each frame:
py-spy dump --pid 12345The permissions wall and other ways py-spy fails quietly
Reading another process's memory is a privileged operation, and the failure mode is rarely a clean error. On Linux, ptrace and process_vm_readv are governed by the yama ptrace_scope setting and by the user the target runs as. A profiler attached as a different user, or against a process in a container with a restricted seccomp profile, can end up with no usable stack. The README does not document a rollback or a permissions troubleshooting path, so the practical check is to run py-spy dump against a known-good process before you need it on an incident.
The second limitation is native code. py-spy can profile extensions written in C, C++ or Cython, but only with the --native flag, and the README's platform table shows that support is uneven: x86-64 works on Linux and Windows, ARM on Linux, Aarch64 on Linux, with macOS and FreeBSD entries that the truncated table does not confirm. For Cython, line numbers in the original .pyx file require the generated C or C++ file to be present. Without symbols, native frames degrade to addresses.
Sampling itself is the third constraint. A sampling profiler observes rather than records, so short-lived functions can be missed entirely and the profile is statistical. The README does not state a default sampling rate in the text here; it points to py-spy record --help. If you need exact call counts, this is the wrong tool by design.
py-spy compared with in-process profilers like pyinstrument
The difference is where the profiler lives. pyinstrument and similar tools run inside the target interpreter, which lets them see every call precisely and attribute time to individual lines, but it also means the target must import them and pay their cost. py-spy stays outside, reads memory, and samples, so it sees a statistical picture at low overhead and cannot be used to count calls exactly.
That trade-off decides the use case. During development, when you control the process and can restart it freely, an in-process profiler gives you more detail per run. On a production host, when the process is serving traffic and you have a PID, py-spy is the one that will not change the thing you are measuring. The README frames the whole project around that distinction, arguing that profilers running inside the target "will usually have a noticeable impact on performance." It is a fair framing, and it also tells you when not to reach for py-spy: if you need deterministic per-line attribution, sampling will frustrate you.
Maintenance, releases and the MIT licence
The repository is not archived. The last push was on 2026-08-14, which is recent enough that the codebase is being touched, but the release cadence is slow and worth planning around. v0.4.2 landed on 2026-04-24, v0.4.1 on 2025-07-31, and v0.4.0 on 2024-11-01. Cargo.toml carries version 0.4.2, matching the latest release. If you pin py-spy in a build image, expect to sit on a version for months at a time.
The licence is MIT, declared in both LICENSE and the Cargo.toml license field, and the PyPI classifier is "License :: OSI Approved :: MIT License." MIT is permissive: you can bundle the binary, ship it internally, or include it in a product. The usual obligation is preserving the copyright notice and licence text. That is a general description of the licence, not legal advice for your situation.
Upgrade cost is low in practice. py-spy is a single binary with no runtime dependency on the profiled interpreter, so bumping it does not touch your application's dependency graph. The real upgrade risk is ABI drift: new CPython versions require new bindgen-generated layouts, and the README's supported range (2.3 to 2.7 and 3.3 to 3.14) is the thing to re-check when you move to a newer interpreter.
When py-spy is the wrong tool
Three cases stand out. First, memory. py-spy samples call stacks and does not measure allocations, so if the question is why resident memory keeps climbing, a profiler built for allocation tracking answers it and py-spy does not.
Second, exactness. If you need to know that a function was called 4,182 times, sampling gives you a distribution, not a count. The README's own framing is about seeing where time goes, not about counting events.
Third, environments where the read is blocked. Hardened containers, seccomp filters, and cross-user attachments can all prevent the memory read. The README documents the mechanism and the symbol fallback but does not document a permissions troubleshooting procedure, so a failed attach in a locked-down environment is a dead end rather than a configuration problem you can fix from the docs. Test the attach path in a staging copy of the production runtime before you rely on it during an incident.
Editorial conclusion
Adopt py-spy when you need a profile of a process you cannot restart or edit, especially on a production host where the code is already running and you only have a PID. Do not adopt it as a replacement for a deterministic line-level profiler during development, and do not expect it to explain native allocation or memory growth, which it does not measure. Before trusting a profile, confirm the sampling rate with py-spy record --help, check whether --native is needed for your C extensions, and on Linux verify that ptrace or process_vm_readv access is actually permitted for the target user, because an attached profiler that silently reads nothing is worse than no profile at all.
Frequently asked questions
Is py-spy safe to run against a production Python process?
The README states that py-spy is written in Rust, does not run in the same process as the profiled program, and is therefore safe to use against production Python code. It reads memory from outside the target rather than injecting code or patching bytecode. Whether the read succeeds depends on the permissions of the user running py-spy.
How do I install py-spy?
The README gives pip install py-spy for prebuilt binary wheels from PyPI, with prebuilt binaries also available on the GitHub Releases page. Rust users can run cargo install py-spy, which builds from source and needs libunwind on Linux and Windows. macOS has a Homebrew formula, Arch Linux an AUR package, and Alpine a testing repository package.
How do I use py-spy to profile a running program?
py-spy works from the command line and accepts either a PID or a command to launch. The README shows py-spy record -o profile.svg --pid 12345 to write a flame graph, py-spy top --pid 12345 for a live view, and py-spy dump --pid 12345 to print the current call stack of each thread.
What are the differences between py-spy and pyinstrument?
pyinstrument runs inside the profiled interpreter, so it can attribute time precisely but must be imported by the target and adds overhead. py-spy stays outside the process, reads memory with process_vm_readv, vm_read or ReadProcessMemory, and samples, which is why the README positions it for profiling code you cannot restart or modify.
What does py-spy do?
The README describes py-spy as a sampling profiler for Python programs that lets you visualize what a Python program is spending time on without restarting it or modifying the code. It offers three subcommands, record, top and dump, and works on Linux, OSX, Windows and FreeBSD.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/benfred-py-spy)