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.
Install py-spy
Install py-spy as a standalone tool on PATH:
uv tool install py-spyConfirm the install and see the available subcommands:
py-spy --version
py-spy --helpWrite 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:
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/pythonRecord a Flame Graph
Run the script under py-spy and record a flame-graph SVG:
py-spy record -o profile.svg -- $PY slow.pyThe -- 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.pyOpen 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 LinuxAttach and record for 30 seconds:
sudo py-spy record --pid "$(pgrep -f slow.py)" --duration 30 -o live.svgOn 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.pyThe equivalent on Kubernetes is securityContext.capabilities.add: [SYS_PTRACE] on the pod spec.
Learn More
- py-spy reference
- uv reference
- Speedscope
- py-spy GitHub repository