py-spy profiles Python processes without modifying or restarting them. pip install py-spy. Top (live): py-spy top --pid <PID>. Record flamegraph: py-spy record -o profile.svg --pid <PID>. Duration: py-spy record -o p.svg --pid <PID> --duration 30. New process: py-spy record -o p.svg -- python script.py. Speedscope: py-spy record -o p.json --format speedscope --pid <PID>. Dump: py-spy dump --pid <PID> — one-shot stack snapshot to stdout. Native: py-spy record --native --pid <PID> — include C extension frames. Subprocesses: py-spy record --subprocesses --pid <PID>. Rate: py-spy record --rate 100 --pid <PID> — 100 samples/sec (default 100). Idle: py-spy record --idle --pid <PID> — include threads waiting on I/O. GIL: py-spy top --gil --pid <PID> — show GIL contention. Threads: py-spy dump --pid <PID> --full — all threads and their current frame. Docker: docker exec <ctr> py-spy top --pid 1. py-spy record --pid $(pgrep -f myapp.py) -o /tmp/profile.svg. Output SVG: default. HTML: --format html. Programmatic: import py_spy; stacks = py_spy.get_stacks(pid=<PID>). Claude Code generates py-spy profiling scripts, process monitoring automation, and production flamegraph capture tools.
CLAUDE.md for py-spy
## py-spy Stack
- Version: py-spy >= 0.3 | pip install py-spy
- Record: py-spy record -o profile.svg --pid <PID> [--duration 30]
- New process: py-spy record -o profile.svg -- python script.py
- Top: py-spy top --pid <PID> — htop-style live view
- Dump: py-spy dump --pid <PID> — instant stack snapshot
- Native: --native | Subprocesses: --subprocesses | Idle: --idle
py-spy Profiling Pipeline
# app/pyspy_utils.py — py-spy record, dump, top, and automation helpers
from __future__ import annotations
import os
import shutil
import signal
import subprocess
import sys
import time
from pathlib import Path
from typing import Any
# ─────────────────────────────────────────────────────────────────────────────
# 1. Check py-spy availability
# ─────────────────────────────────────────────────────────────────────────────
def _pyspy() -> str:
"""Return the py-spy executable path, raising if not found."""
exe = shutil.which("py-spy")
if exe:
return exe
# Try module invocation
result = subprocess.run(
[sys.executable, "-m", "py_spy", "--version"],
capture_output=True,
)
if result.returncode == 0:
return f"{sys.executable} -m py_spy"
raise RuntimeError(
"py-spy not found. Install with: pip install py-spy\n"
"Note: recording may require sudo on Linux."
)
def pyspy_available() -> bool:
"""Return True if py-spy is installed and accessible."""
try:
_pyspy()
return True
except RuntimeError:
return False
# ─────────────────────────────────────────────────────────────────────────────
# 2. Record — attach to running process
# ─────────────────────────────────────────────────────────────────────────────
def record_pid(
pid: int,
output: str | Path = "/tmp/profile.svg",
duration: int | None = 30,
rate: int = 100,
native: bool = False,
subprocesses: bool = False,
idle: bool = False,
fmt: str = "svg",
) -> Path:
"""
Record a flamegraph for an already-running Python process.
Attaches non-intrusively via ptrace (Linux) / task_info (macOS).
pid: PID of the Python process to profile.
output: output file path (extension ignored; use fmt).
duration: seconds to record (None = record until Ctrl+C).
rate: samples per second (default 100).
native: include C extension frames.
subprocesses: also profile child processes.
idle: include threads blocked on I/O or sleep.
fmt: "svg", "html", "speedscope", "raw".
Note: may require `sudo` on Linux unless kernel.perf_event_paranoid ≤ 1.
Returns path to the generated file.
"""
out = Path(output).with_suffix(f".{fmt}")
cmd = ["py-spy", "record", "-o", str(out), "--format", fmt,
"--rate", str(rate), "--pid", str(pid)]
if duration is not None:
cmd += ["--duration", str(duration)]
if native:
cmd.append("--native")
if subprocesses:
cmd.append("--subprocesses")
if idle:
cmd.append("--idle")
subprocess.run(cmd, check=True)
return out
def record_script(
script: str | list[str],
output: str | Path = "/tmp/profile.svg",
rate: int = 100,
native: bool = False,
fmt: str = "svg",
) -> Path:
"""
Launch a Python script under py-spy and record until it exits.
script: single python file path, or list of args like ["python", "app.py", "--arg"].
Example:
path = record_script("benchmarks/sort_bench.py", output="/tmp/sort.svg")
"""
out = Path(output).with_suffix(f".{fmt}")
cmd = ["py-spy", "record", "-o", str(out), "--format", fmt, "--rate", str(rate)]
if native:
cmd.append("--native")
if isinstance(script, str):
cmd += ["--", sys.executable, script]
else:
cmd += ["--"] + list(script)
subprocess.run(cmd, check=True)
return out
# ─────────────────────────────────────────────────────────────────────────────
# 3. Dump — instant stack snapshot
# ─────────────────────────────────────────────────────────────────────────────
def dump_stacks(
pid: int,
all_threads: bool = True,
locals_: bool = False,
) -> str:
"""
Dump the current call stacks of a running Python process.
Returns the text output as a string.
Useful for: instant diagnosis of a stuck/hanged process.
Example:
stacks = dump_stacks(os.getpid())
print(stacks[:2000])
"""
cmd = ["py-spy", "dump", "--pid", str(pid)]
if all_threads:
cmd.append("--full")
result = subprocess.run(cmd, capture_output=True, text=True)
return result.stdout + result.stderr
def dump_own_stacks() -> str:
"""Dump stacks of the current Python process (self-profiling)."""
return dump_stacks(os.getpid())
# ─────────────────────────────────────────────────────────────────────────────
# 4. Timed profiling — record for N seconds then return path
# ─────────────────────────────────────────────────────────────────────────────
class TimedRecorder:
"""
Attach py-spy to the current process for a fixed duration in a thread.
Note: self-profiling on Linux requires SYS_PTRACE capability or sudo.
Usage:
recorder = TimedRecorder(duration=10, output="/tmp/api_50s.svg")
recorder.start()
# ... do work ...
recorder.wait()
print(f"Flamegraph: {recorder.output_path}")
"""
def __init__(
self,
duration: int = 30,
output: str | Path = "/tmp/timed_profile.svg",
rate: int = 100,
native: bool = False,
fmt: str = "svg",
) -> None:
self.duration = duration
self.output_path = Path(output).with_suffix(f".{fmt}")
self.rate = rate
self.native = native
self.fmt = fmt
self._proc: subprocess.Popen | None = None
def start(self) -> None:
"""Start recording asynchronously."""
cmd = [
"py-spy", "record",
"-o", str(self.output_path),
"--format", self.fmt,
"--rate", str(self.rate),
"--duration", str(self.duration),
"--pid", str(os.getpid()),
]
if self.native:
cmd.append("--native")
self._proc = subprocess.Popen(cmd, stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL)
def stop(self) -> None:
"""Interrupt recording early."""
if self._proc and self._proc.poll() is None:
self._proc.send_signal(signal.SIGINT)
def wait(self, timeout: float | None = None) -> None:
"""Wait for recording to complete."""
if self._proc:
self._proc.wait(timeout=timeout)
# ─────────────────────────────────────────────────────────────────────────────
# 5. Docker / Kubernetes helper snippets
# ─────────────────────────────────────────────────────────────────────────────
DOCKER_EXAMPLES = """
# Profile PID 1 inside a running container for 30 seconds:
docker exec -it <container_id> py-spy record -o /tmp/profile.svg --pid 1 --duration 30
# Copy the flamegraph out:
docker cp <container_id>:/tmp/profile.svg ./profile.svg
# One-shot stack dump (no file):
docker exec <container_id> py-spy dump --pid 1
# Kubernetes pod:
kubectl exec <pod> -- py-spy record -o /tmp/profile.svg --pid 1 --duration 30
kubectl cp <pod>:/tmp/profile.svg ./profile.svg
"""
GITHUB_ACTIONS_EXAMPLE = """
# In CI: profile a benchmark script and upload the flamegraph as an artifact
- name: Run profiled benchmark
run: |
pip install py-spy
py-spy record -o profile.svg -- python benchmarks/bench_main.py
- uses: actions/upload-artifact@v4
with:
name: flamegraph
path: profile.svg
"""
# ─────────────────────────────────────────────────────────────────────────────
# Demo
# ─────────────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
if not pyspy_available():
print("py-spy not installed. Run: pip install py-spy")
sys.exit(1)
print("=== Profile current script ===")
out = record_script(
[sys.executable, "-c",
"import time; [sum(range(100_000)) for _ in range(50)]; time.sleep(0.1)"],
output="/tmp/demo_profile.svg",
rate=200,
)
print(f" Flamegraph: {out} ({out.stat().st_size:,} bytes)")
print("\n=== Stack dump (current process) ===")
stacks = dump_own_stacks()
lines = stacks.strip().splitlines()
print(f" {len(lines)} lines of stack info")
for line in lines[:8]:
print(f" {line}")
print("\nDone. Open the SVG in a browser to explore the flamegraph.")
For the cProfile / pyinstrument alternative — cProfile and pyinstrument both require you to restart the process wrapped in their instrumentation; py-spy attaches to any running PID at any time with zero code changes, making it the only option for profiling production processes, stuck workers, or third-party code that you cannot modify. For the gdb / strace alternative — gdb and strace can dump C-level stacks but cannot map them to Python frames; py-spy understands CPython internals and translates memory addresses to Python function names, file paths, and line numbers — giving you actionable Python-level information from a binary inspection. The Claude Skills 360 bundle includes py-spy skill sets covering record_pid() with duration/rate/native/subprocesses/idle, record_script() for profiling a new process, dump_stacks()/dump_own_stacks() instant snapshot, TimedRecorder async recorder class, Docker/Kubernetes exec snippets, GitHub Actions CI profiling pattern, speedscope JSON output format, and GIL contention monitoring with —gil flag. Start with the free tier to try production process profiling code generation.