Statistical Profiling in Python, Part 3: Tooling
The first two posts in this series were about collecting data: CPU and wall-clock samples from a signal handler, and heap samples from inside the allocator. Both produce a pile of stack traces with counts attached.
A pile of stack traces is not an answer. To be useful it has to be reachable from outside the process, and it has to be visualized in a way that a developer can reason about it. This last post is about that plumbing.
pypprof adds HTTP endpoints to a running Python application, modeled directly on Go’s net/http/pprof. With these endpoints, you can use the pprof tooling as if your application were written in Go.
from pypprof.net_http import start_pprof_server
start_pprof_server(port=8081)
That starts a small HTTP server on a daemon thread, serving:
| Endpoint | What it gives you |
|---|---|
/debug/pprof/profile?seconds=30 | CPU profile |
/debug/pprof/wall?seconds=30 | Wall-clock profile |
/debug/pprof/heap?gc=1 | Live heap snapshot |
/debug/pprof/thread?debug=1 | Every thread’s stack |
/debug/pprof/goroutine | The same thing, under a name Go’s tools expect |
/debug/pprof/cmdline | The process’s argv |
The paths, the query parameters, and the ?debug=1 plain-text mode all mirror Go. That means:
$ go tool pprof -http=:8088 :8081/debug/pprof/profile
$ go tool pprof :8081/debug/pprof/heap
$ curl localhost:8081/debug/pprof/thread?debug=1
works against a Python process. The flame graph, the call graph, the source view, the diff between two profiles — none of it had to be written, because the Python service is now indistinguishable from a Go one as far as go tool pprof is concerned. The goroutine alias is the most shameless example: Python has no goroutines, but tools go looking for that path, and answering with thread stacks is more useful than being pedantic.
Update (2026): Python caught up
Python has not stood still since 2019, and the gap I was writing about has largely closed from the other direction.
tracemalloc, introduced by PEP 454, remains the standard-library answer for tracing Python allocations and is still the right tool when exact allocation histories matter. PEP 669, “Low Impact Monitoring for CPython”, added a much cheaper monitoring interface for tools that need interpreter events without turning every line into a callback — the thing that made cProfile-style tooling so expensive.
The bigger change is statistical execution profiling arriving in the standard library. Python 3.15 adds the profiling package and profiling.sampling, a built-in sampler that can attach to an already running process and produce pstats, flame graphs, and other views. That is squarely the problem this series was about, solved properly, by people with the ability to change the interpreter rather than work around it — which is a much better place to solve it from. It addresses where time goes rather than where heap bytes live, so mprofile is not entirely redundant, but I would reach for the built-in tools first now.