Skip to content

How to profile a Python script with py-spy

This guide shows how to use py-spy to profile a Python script. py-spy is a sampling profiler that runs in a separate process from the program it profiles, so it needs no import, no decorator, and no restart. The result is a flame graph that shows where CPU time is actually spent.

This guide assumes uv is installed. Follow the uv installation guide first if needed.

Install py-spy

Install py-spy as a standalone tool on PATH:

uv tool install py-spy

Confirm the install and see the available subcommands:

py-spy --version
py-spy --help

Write a Script to Profile

Save the following as slow.py. It spends most of its time inside slow_hash, which is the function the profile should highlight:

slow.py
import hashlib
import random


def slow_hash(data: bytes, rounds: int) -> str:
    digest = data
    for _ in range(rounds):
        digest = hashlib.sha256(digest).digest()
    return digest.hex()


def fast_work(n: int) -> int:
    return sum(i * i for i in range(n))


def main() -> None:
    for _ in range(50):
        payload = random.randbytes(1024)
        slow_hash(payload, rounds=200_000)
        fast_work(10_000)


if __name__ == "__main__":
    main()

Create a Virtual Environment

py-spy reads state directly from a Python interpreter process, so it needs to launch the interpreter itself rather than a wrapper. Create a virtual environment and use its Python binary:

uv venv
PY=.venv/bin/python

Record a Flame Graph

Run the script under py-spy and record a flame-graph SVG:

py-spy record -o profile.svg -- $PY slow.py

The -- separator tells py-spy that everything after it is the command to launch. py-spy starts the Python process as a child, so no elevated permissions are needed on Linux or Windows (macOS always requires root; see Attach to a Running Process).

Open profile.svg in any browser. The widest bar on the flame graph should be slow_hash, because that is where the script spends most of its CPU time.

View the Profile in Speedscope

Flame-graph SVGs work in a browser, but speedscope offers a richer interactive view with time-ordered, left-heavy, and sandwich layouts. Switch the output format:

py-spy record --format speedscope -o profile.json -- $PY slow.py

Open speedscope.app and drag profile.json onto the page. The same file also loads in the Perfetto UI as a Chrome trace.

Attach to a Running Process

py-spy can also attach to a Python process that is already running. Find its PID:

pgrep -f slow.py          # macOS and Linux

Attach and record for 30 seconds:

sudo py-spy record --pid "$(pgrep -f slow.py)" --duration 30 -o live.svg

On Linux, sudo is required unless kernel.yama.ptrace_scope has been relaxed. On macOS, attaching always requires root, and the system Python at /usr/bin/python cannot be profiled; use a uv-installed or Homebrew Python instead.

Dump Live Stacks from a Hung Process

When a Python process is stuck rather than slow, a full profile is overkill. py-spy dump prints the current call stack for every thread, which is often enough to spot the problem:

sudo py-spy dump --pid "$(pgrep -f slow.py)"

Profile in Docker

The default Docker seccomp profile blocks the system call py-spy uses to read another process’s memory. Run the container with the SYS_PTRACE capability so py-spy can attach:

docker run --cap-add SYS_PTRACE --rm -it my-python-image py-spy record -o profile.svg -- python slow.py

The equivalent on Kubernetes is securityContext.capabilities.add: [SYS_PTRACE] on the pod spec.

Learn More

Last updated on

Please submit corrections and feedback...