py-spy 0.4.2

py-spy: see what a running Python process is doing

py-spy is a sampling profiler that attaches to a running Python process by PID, without restarting it or changing your code. It samples 100 times per second by default, py-spy dump --pid 12345 prints every thread's stack, and version 0.4.2 supports CPython 2.3-2.7 and 3.3-3.14. In containers, attaching needs --cap-add SYS_PTRACE.

4 min readLogs and monitoring

At a glance

  • py-spy dump --pid 12345 prints a stack trace of a stuck process right now.
  • py-spy top --pid 12345 shows live function times and a %GIL column in the terminal.
  • py-spy record -o profile.svg --pid 12345 --duration 30 writes a 30-second flame graph.
  • In Docker, add --cap-add SYS_PTRACE or run py-spy from the host.

Install py-spy

Install py-spy from PyPI as a prebuilt binary wheel with pip install py-spy. On macOS you can also use brew install py-spy, and Rust users can run cargo install py-spy. The latest release is 0.4.2, published April 24, 2026.

Bash
pip install py-spy
py-spy --version

py-spy runs on Linux, macOS, Windows and FreeBSD. It is written in Rust and runs in a separate process from your program, reading its memory without interrupting or modifying your Python code, which is why its README calls it safe to use against production code.

dump, top and record

py-spy dump prints the current call stack of every Python thread once and exits. py-spy top opens a live view of the functions taking the most time, like Unix top, with a %GIL column. py-spy record samples over time and writes a profile to a file, an interactive SVG flame graph by default.

CommandWhat it showsUse it when
py-spy dump --pid 12345The current stack of every thread, onceA job looks stuck and you want to see where
py-spy top --pid 12345Live view of the functions using the most timeYou want to watch a slow process right now
py-spy record -o profile.svg --pid 12345Interactive SVG flame graphYou want to find where the time goes
py-spy record --format speedscope -o profile.json --pid 12345A profile for the speedscope viewerYou want to zoom into a long profile
py-spy record -o profile.svg -- python train.pyA flame graph of a program py-spy startsYou can't attach as root on Linux

record and top can attach to a running process with --pid or start a new program after --; dump only attaches, with --pid. With record, --duration stops after a fixed number of seconds, or press Ctrl+C to stop and write the file.

Bash
py-spy dump --pid 12345
py-spy top --pid 12345
py-spy record -o profile.svg --pid 12345 --duration 30
py-spy record -o profile.svg -- python train.py

Read the output

A dump starts with the process and interpreter lines, then lists each thread with its frames, newest call first, and marks idle threads in the thread header:

Terminal
Process 3041: python job.py
Python v3.13.14 (/usr/local/bin/python3.13)

Thread 3041 (idle): "MainThread"
    load_batch (job.py:5)
    train (job.py:11)
    <module> (job.py:13)

In the flame graph from record, wider boxes took more of the sampled time. py-spy samples at a --rate of 100 per second and leaves out idle threads by default: in our test, a 3-second recording of a script that spends its time in time.sleep wrote 0 samples by default and 299 with --idle.

The flags that change what gets captured:

  • --idle includes idle threads, which are excluded by default.
  • --gil only includes threads that hold the GIL.
  • --native adds stacks from Cython, C and C++ extensions (Linux and Windows on x86-64, plus Linux on ARM).
  • --subprocesses profiles child processes too, for multiprocessing or gunicorn worker pools.
  • --nonblocking doesn't pause the target while sampling, at the cost of occasional sampling errors.
  • --locals makes dump print the local variables of each frame.

Fix Permission Denied

Linux and macOS

On most Linux systems, attaching to a process that py-spy did not start requires root because of the kernel.yama.ptrace_scope setting. If you can start the job yourself, launching it under py-spy (-- python myprogram.py) avoids the need for root. macOS always requires root, and System Integrity Protection blocks profiling the system Python in /usr/bin.

Without enough privileges, py-spy prints the fix itself. sudo env "PATH=$PATH" keeps your virtualenv's py-spy on the path under sudo:

Terminal
$ py-spy dump --pid 12345
Permission Denied: Try running again with elevated permissions by going 'sudo env "PATH=$PATH" !!'
$ sudo env "PATH=$PATH" py-spy dump --pid 12345

Docker and Kubernetes

Inside a Docker container, py-spy usually fails with a permission error even as root, because Docker restricts the process_vm_readv call py-spy uses. Start the container with --cap-add SYS_PTRACE (or cap_add in Compose) and restart it, or run py-spy on the host against the container's process.

Kubernetes drops SYS_PTRACE by default as well. Add it under the container's securityContext (changing a Deployment re-creates its pods), or attach an ephemeral debug container with kubectl debug.

Bash
docker run --cap-add SYS_PTRACE -it my-image
kubectl debug --profile=general -n <namespace> --target=<container> <pod> --image=python:3.12-slim -it -- bash
YAML
securityContext:
  capabilities:
    add:
      - SYS_PTRACE

Pair a profile with a progress bar

py-spy shows which functions burn CPU or wait, but not which step a long training run or batch job is on, or when it will finish. A progress bar answers that, and you can use both on the same job.

FAQ

Does py-spy slow down the program it profiles?

Very little. py-spy runs in a separate process and reads memory from the outside without instrumenting your code. By default it briefly pauses the target for each sample, 100 times per second; pass --nonblocking to never pause it, at the cost of occasional sampling errors.

Why does py-spy say Permission Denied inside Docker even as root?

Docker restricts the process_vm_readv system call that py-spy uses to read another process's memory. Add --cap-add SYS_PTRACE to docker run (or cap_add in Compose) and restart the container, or run py-spy as root on the host instead.

Which Python versions does py-spy support?

py-spy 0.4.2 supports CPython 2.3 to 2.7 and 3.3 to 3.14, on Linux, macOS, Windows and FreeBSD.

Can py-spy profile C extensions and multiprocessing workers?

Yes. Pass --native to include stacks from C, C++ and Cython extensions, and --subprocesses to profile child processes started by multiprocessing or gunicorn worker pools.

How is py-spy different from cProfile?

cProfile is the deterministic profiler in the Python standard library, and you start your script with it, for example python -m cProfile script.py, because it runs inside the process from the beginning. py-spy is an external sampling profiler that can attach to a process that is already running.

Check on long runs from your phone

Change one import and every tqdm bar prints a link you can open on any device. Get a Discord or Slack message when the run finishes, fails or stalls.

Get a live linkpip install tqdm-live

Sources