Open-source project
bloomberg/pystack avatar
bloomberg/pystack

PyStack: read the Python stack of a live process or a core dump without a debugger

🔍 🐍 Like pstack but for Python!

1,225 stars74 forksC++Apache-2.0

At a glance

What is it?
PyStack is a Bloomberg tool that inspects the Python stack frames of a running process or a core dump by reading process memory rather than executing anything in it. It is Linux only, ships as a C++ extension with a Python front end, and needs elfutils libraries to build from source.
Who is it for?
PyStack is the right tool when you need to know what a running Python service is doing, or what it was doing when it crashed, and you want Python frames rather than a wall of CPython internals, especially on a production host where attaching GDB is unwelcome. It is the wrong tool on macOS or Windows, since only Linux is supported, and on a stripped or aggressively optimized binary it will fall back to the Python stack alone.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 10 days ago.
What is it written in?
Mainly C++, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 24, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Reading the stack without touching the process

The problem PyStack addresses is specific: a Python process is stuck, or a service is behaving strangely, and you want to know where. The usual answer is a debugger, and the usual difficulty is that a debugger changes the thing you are trying to observe and requires privileges the production box may not allow.

PyStack takes the position that it does not need to be a debugger at all. The project describes it as a tool that uses forbidden magic to let you inspect the stack frames of a running Python process or a Python core dump, helping you learn what the process is doing, or what it was doing when it crashed, without having to interpret CPython internals yourself. That last clause is the real product. A GDB backtrace of a Python process shows you the interpreter's own C frames, and reading them requires knowing how the eval loop calls into your code.

The safety property is stated explicitly in the feature list. PyStack does not modify any memory or execute any code in a process that is running. It simply attaches just long enough to read some of the process's memory. The remote subcommand documentation repeats it: the analysis is always done in a safe and non-intrusive way, as no code is loaded in the memory space of the process under analysis and no memory is modified in the remote process.

That makes it usable in environments where the running process must not be impacted in any way, other than being temporarily paused. The `--no-block` flag goes further and avoids even the pause.

Two subcommands, one for a live PID and one for a core file

PyStack is organised around two subcommands, and the top level help text makes the split clear:

bash
usage: pystack [-h] [-v] [--no-color] {remote,core} ...

The `remote` command analyses a running process given its PID, and the `core` command analyses a core dump file given its location and the executable. The `core` path exists because the most common hard problem in production is a process that died, and by then you have a core file and a symbol table rather than a live process to poke at.

The `remote` command has a set of options that determine how much detail you get and how much you interfere:

bash
usage: pystack remote [-h] [-v] [--no-color] [--no-block] [--native] [--native-all] [--locals] [--exhaustive] pid

`--native` includes the native C frames in the resulting stack trace, `--native-all` includes native frames from threads not registered with the interpreter and implies `--native`, `--locals` shows local variables for each frame, and `--no-block` runs the analysis without pausing the process while its memory is inspected. There is also an `--exhaustive` flag described as using all possible methods to obtain the Py frames.

One option deserves attention because it changes the reliability model rather than the output detail. The project notes that PyStack can optionally perform a Python stack analysis without pausing the process at all, which minimizes impact to the debugged process at the cost of potentially failing due to data races. That is an honest trade, and it is the one to reach for when the process cannot be interrupted but you accept that the answer may be wrong or unavailable.

The GIL state of every thread is the first thing you read

The feature list leads with a few capabilities that are not about stack depth at all, and they are the ones that answer the most common Python production question, which is why everything is slow.

PyStack shows if each thread currently holds the Python GIL, is waiting to acquire it, or is currently dropping it. It also shows if a thread is running a garbage collection cycle. Those two facts together explain a large share of latency complaints that have nothing to do with a deadlock. A thread waiting to acquire the GIL is not blocked on I/O, it is waiting for CPU-bound Python code elsewhere. A thread in a garbage collection cycle is not stuck, it is doing work that the framework can raise or tune.

It also reports on multiple interpreters in the same process, which matters for deployments that embed CPython in a host application or run several interpreters in one process for isolation.

The native mode is more careful than it first appears. When PyStack optionally shows native function calls as well as Python ones, it prints the native stack trace of C, C++, and Rust function calls, except that calls to Python callables are replaced with frames showing the Python code being executed rather than the internal C code the interpreter used to make the call. In other words, the native frames are there to tell you that a Python call ended up in a library, and the call site is what you get back.

Two more native-mode details matter when you are reading a binary that was built with optimisation. Symbols shown in the native stack are automatically demangled, and calls to inlined functions are included in the native stack whenever enough debug information is available.

Building from source needs libdw and libelf

PyStack is published on PyPI, so for most people installation is a pip command and nothing more. The from-source path is documented for anyone who needs to modify it, and the binary dependencies are the first hurdle: libdw and libelf, which are sometimes provided together as part of a distribution's `elfutils` package.

The documentation gives a Debian-based example:

bash
apt-get install libdw-dev libelf-dev

and mentions `pkg-config`, which, when available, is used automatically to locate the libraries and configure the correct build flags. You may need to tell the compiler where to find the header and library files otherwise. On Alpine Linux, or any distribution that does not use glibc, the requirement is elfutils 0.188 or newer, and you may need to build it from source if the package manager does not offer it.

Once the dependencies are in place, the build follows the usual Python library process:

bash
git clone [email protected]:bloomberg/pystack.git pystack
cd pystack
python3 -m venv ../pystack-env/  # just an example, put this wherever you want
source ../pystack-env/bin/activate
python3 -m pip install --upgrade pip
python3 -m pip install -e . --group test --group extra

The `-e` puts the package into the virtual environment in development mode, and the `--group test --group extra` flags pull in the libraries needed to test, lint, and document it. If you intend to contribute, the project asks you to install the pre-commit hooks:

bash
prek install

The build system itself is worth noting, because it explains the packaging. pyproject.toml declares a `scikit-build-core` backend with `nanobind`, and the project is configured to build against the CPython Limited API and the stable ABI so that the resulting binary works across CPython versions. That is a deliberate choice, and it is the reason the same wheel can serve Python 3.9 through 3.15, the range listed in the classifiers. The package itself is C++ with nanobind bindings, with a Python front end whose console script is `pystack = "pystack.__main__:main"`.

Stripped binaries and optimized interpreters are the hard cases

PyStack claims to work in conditions where most tooling gives up, and the two interesting claims are about degradation rather than capability.

It works with aggressively optimized Python interpreter binaries, and it works with Python interpreter binaries that do not have symbols or debug information, in which case you get the Python stack only. That second case is the common one in the field, because distributions ship stripped interpreters. The consequence is precise: the Python frames are still recovered, but the native frames are not, so you lose the ability to see that a Python call went into a C extension. The tool does not fail, it gives you less.

The other claim is about failure. PyStack tolerates memory corruption well: even if the process crashed because of memory corruption, it can usually reconstruct the stack. That is a different kind of resilience, since a corrupted heap is exactly the situation where a debugger's own view of the process is unreliable. Being able to get a stack out of a process that died from corruption is often the difference between a diagnosis and a shrug.

There is a performance claim as well, that PyStack can analyse core files 10x faster than general-purpose tools like GDB. Treat that as a property of the tool as described rather than a measurement, since no benchmark procedure is given, but the mechanism behind it is structural. PyStack is self-contained and does not depend on external tools or programs other than the Python interpreter used to run PyStack itself, so it is not paying for a general-purpose debugger's flexibility when it only needs to walk one structure.

Linux only, and a container workflow for everything else

The platform answer is short: at this time only Linux is supported. That is a hard boundary, not a gap to be filled later, and it follows from the design. The tool works by attaching to another process and reading its memory, and the mechanism it uses for that is Linux specific.

For a team whose debugging happens on macOS laptops, the workaround is in the repository rather than in the documentation. There is a Dockerfile and a set of Makefile targets for the container workflow, including `docker-build` to build the image, `docker-shell` to run a shell in it, and `docker-rm` to remove the image. The container is built on `ubuntu:26.04` and installs the build toolchain, gdb, valgrind, lcov, and the `libdw-dev` and `libelf-dev` packages. It installs a range of Python interpreters with uv, from 3.8 through 3.15, each with setuptools, and creates a virtual environment with the test and extra dependency groups.

The `docker-shell` target runs the container with `--privileged`, mounts the current directory at `/src`, and opens a bash shell. That combination is the reason the container path exists: reading another process's memory requires privileges the container grants explicitly.

The Makefile also covers the everyday development targets, with `build` doing an editable install, `dist` producing distribution files, `check` running the test suite through pytest, and `pycoverage` running the same tests with coverage, appending results, and failing under a coverage threshold of 85 percent. The `valgrind.supp` file at the repository root is a suppression file for running the native code under valgrind, which tells you the C++ side is tested with memory checking as part of the workflow.

Editorial conclusion

PyStack is the right tool when you need to know what a running Python service is doing, or what it was doing when it crashed, and you want Python frames rather than a wall of CPython internals, especially on a production host where attaching GDB is unwelcome. It is the wrong tool on macOS or Windows, since only Linux is supported, and on a stripped or aggressively optimized binary it will fall back to the Python stack alone. Before relying on it, check that your build has libdw and libelf available, that the target process is a CPython build PyStack can read, and that your organisation accepts a tool whose whole model is attaching to another process's memory.

Frequently asked questions

How do I print the Python stack of a running process with PyStack?

Use the `remote` subcommand with the PID of the process as its argument. The analysis attaches to the process, reads its memory, and prints the Python stack frames. The `--no-block` flag runs the analysis without pausing the process, at the cost of potentially failing due to data races.

How do I analyse a Python core dump with PyStack?

The `core` subcommand analyses a core dump file given its location and the executable, so you pass it the path to the core file and the binary that produced it. The `remote` subcommand is the one for a process that is still alive.

What does PyStack need to build from source?

The binary dependencies libdw and libelf, which are often part of a distribution's `elfutils` package, for example `apt-get install libdw-dev libelf-dev` on Debian-based systems. On Alpine Linux or any non glibc distribution you need elfutils 0.188 or newer, and `pkg-config` is used to locate the libraries when it is available.

Does PyStack work on macOS or Windows?

No. The project states that at this time only Linux is supported. On other systems the repository provides a Docker workflow with Makefile targets such as `docker-build` and `docker-shell` that run the tool inside a container built on ubuntu:26.04.

Can PyStack show local variables and native C frames?

Yes. The `--locals` flag shows local variables for each frame in the stack trace, `--native` includes the native C frames, and `--native-all` additionally includes native frames from threads not registered with the interpreter, implying `--native`.

Official sources

  1. bloomberg/pystack on GitHub
  2. License: Apache-2.0
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/bloomberg-pystack.svg)](https://hysenlabs.com/projects/bloomberg-pystack)

Was this useful?