# How to profile a Python script with py-spy


This guide shows how to use [py-spy](https://pydevtools.com/handbook/reference/py-spy.md) 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.

{{< callout type="warning" >}}
This guide assumes [uv](https://pydevtools.com/handbook/reference/uv.md) is installed. Follow the [uv installation guide](https://pydevtools.com/handbook/how-to/how-to-install-uv.md) first if needed.
{{< /callout >}}

## Install py-spy

Install py-spy as a standalone tool on `PATH`:

```bash
uv tool install py-spy
```

Confirm the install and see the available subcommands:

```bash
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:

```python {filename="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:

```bash
uv venv
PY=.venv/bin/python
```

```powershell
uv venv
$PY = ".venv\Scripts\python.exe"
```

## Record a Flame Graph

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

```bash
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](#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](https://www.speedscope.app/) offers a richer interactive view with time-ordered, left-heavy, and sandwich layouts. Switch the output format:

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

Open [speedscope.app](https://www.speedscope.app/) and drag `profile.json` onto the page. The same file also loads in the [Perfetto UI](https://ui.perfetto.dev/) 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:

```bash
pgrep -f slow.py          # macOS and Linux
```

Attach and record for 30 seconds:

```bash
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](https://pydevtools.com/handbook/reference/uv.md)-installed or Homebrew Python instead.

```powershell
py-spy record --pid <PID> --duration 30 -o live.svg
```

Run PowerShell as Administrator when attaching to a process started by another user.

## 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:

```bash
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:

```bash
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

- [py-spy](https://pydevtools.com/handbook/reference/py-spy.md) reference
- [uv](https://pydevtools.com/handbook/reference/uv.md) reference
- [Speedscope](https://www.speedscope.app/)
- [py-spy GitHub repository](https://github.com/benfred/py-spy)
