py-spy
py-spy is a Rust sampling profiler that reads CPython interpreter state from outside the target process. It can attach to a running PID or launch a command, then show a live top view, dump every thread's current stack, or record flamegraph, speedscope, raw, and Chrome trace files. Because no profiling code is imported into the application, it is especially useful for production diagnosis and hung processes. Low overhead does not mean zero operational risk: process-memory permissions and brief sampling pauses still matter.
Keep it in the incident-response toolbox for live CPython processes; few profilers get useful evidence with so little application cooperation. Do not normalize permanent elevated privileges just to make attachment convenient.
Use it if
- You need to profile a running Python service without changing code or restarting it
- You want an immediate flamegraph, live top view, or one-shot thread dump from a PID
- You need to include worker subprocesses, GIL-only samples, thread IDs, or supported native extension frames
- You are diagnosing production CPU use and an in-process deterministic profiler would distort the workload
- You need line-by-line CPU and memory diagnostics inside a reproducible run: Scalene or a deterministic profiler provides different, often richer attribution
- Your environment cannot grant process-memory access: attaching to an existing Linux process usually needs ptrace permission, macOS generally needs root, and containers commonly need SYS_PTRACE
- You need portable native-extension stacks: the README lists --native support only on specific architecture and operating-system combinations, and symbols are needed for the best results
- You profile PyPy, 32-bit Windows, or another unsupported interpreter and platform combination: the README explicitly lists these as unsupported
- You need exact call counts or wall-time tracing: a sampling profiler estimates where time is spent and can miss short functions or rare paths
Setup reality
pip install py-spy normally installs a prebuilt binary wheel; the tool runs as the py-spy command and does not need to be imported into the target program. Version 0.4.2 supports recent CPython versions through 3.14 according to the README, but interpreter support and availability of a wheel are separate questions. Cargo installation builds from Rust source and on Linux or Windows requires libunwind development files. Alpine Python does not accept ordinary manylinux wheels, so upstream points to an Alpine testing package or a musl release binary rather than a normal pip path. Attaching is where setup gets operational. py-spy reads another process's memory, so Linux commonly blocks an existing PID under ptrace_scope unless the profiler has matching ownership and permission or runs with elevated privileges. Launching the target as a child of py-spy often avoids that restriction. macOS generally requires root, and System Integrity Protection can prevent profiling the system Python under /usr/bin even as root. Docker's default seccomp and capabilities block the memory-reading call; start the container with SYS_PTRACE or profile from a suitably privileged host. Kubernetes also drops SYS_PTRACE, so adding that capability changes the pod security profile and should go through the cluster's security review. Profile files can reveal function names, paths, process command lines, thread names, and, with dump --locals, application values. Treat them as potentially sensitive artifacts. By default py-spy pauses the process briefly while collecting each sample to avoid inconsistent stack reads. --nonblocking removes that pause but can increase sampling errors and partial frames, and source validation forbids combining it with --native. Sampling defaults to a rate configured by the tool; higher rates increase observation overhead and file volume, so set a bounded duration in production. Native profiling is available only on supported architectures, works best with debug symbols, and Cython line mapping needs generated C or C++ files. --gil intentionally omits extension work that releases the GIL, while --idle does the opposite of its name's common interpretation by including threads py-spy would otherwise classify as idle. For gunicorn or multiprocessing, add --subprocesses from the start so newly created children are followed. Always reproduce a hot path more than once before treating sample proportions as a precise benchmark.
Patterns
Record a flamegraph from a PIDrecord-running-process
py-spy record --pid 12345 --duration 30 --output profile.svgAttaching to an existing process commonly needs ptrace permission on Linux and elevated privileges on macOS.
Launch and profile a Python commandprofile-new-command
py-spy record --output profile.svg -- python -m myservice.workerLaunching the process as py-spy's child often avoids the permission required to attach to an unrelated PID.
Watch hot functions livewatch-live-top
py-spy top --pid 12345 --delay 1.0top is interactive and continuously samples until stopped; use record when you need an artifact for later comparison.
Capture all current thread stacksdump-thread-stacks
py-spy dump --pid 12345A dump is a single snapshot, which is ideal for a hang but can miss intermittent CPU hotspots.
Include local variables in a stack dumpdump-local-values
py-spy dump --pid 12345 --localsLocal variables can contain tokens, personal data, queries, or message bodies; protect and delete the output according to incident policy.
Create a speedscope profilerecord-speedscope
py-spy record --pid 12345 --duration 60 --format speedscope --output profile.jsonOpen the resulting JSON in speedscope; choose flamegraph when a self-contained SVG is easier to share.
Follow worker subprocessesprofile-subprocesses
py-spy record --pid 12345 --subprocesses --duration 60 --output workers.svgThis is useful for multiprocessing and gunicorn; recorded stacks include PID and command information to distinguish workers.
Include supported native extension framesprofile-native-code
py-spy record --pid 12345 --native --duration 30 --output native.svgNative stacks are platform-limited, work best with symbols, and cannot be combined with --nonblocking.
Sample only threads holding the GILisolate-gil-work
py-spy record --pid 12345 --gil --duration 30 --output gil.svgThis can clarify Python CPU contention but omits active extension code that releases the GIL.
Use nonblocking samplingminimize-sampling-pauses
py-spy record --pid 12345 --nonblocking --duration 30 --output nonblocking.svgNonblocking mode avoids pausing the target but can produce more sampling errors or partial stacks, so compare with a normal recording.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| scalene | PyPI | You want line-level CPU, memory, and copy profiling and can run the program under the profiler |
| viztracer | PyPI | You need detailed execution timelines, function events, and an interactive trace viewer |
| pyinstrument | PyPI | You want a friendly in-process statistical profiler for scripts, web requests, or notebooks |