2025, Sep 26 03:00

Understanding NumPy's core: tracing _multiarray_umath shared module (.so) to C/C++ sources with Meson

Learn why NumPy core functions call a compiled _multiarray_umath .so and how to trace it to C/C++ sources using Meson introspect for debugging and audits.

Peeking into NumPy’s core and not finding a plain Python file for functions like concatenate or inner is a rite of passage. The reason is simple: those call into a compiled shared module that ships as a .so, not a .py. Understanding what that file is, where it comes from, and how to trace it back to source will make reading and debugging NumPy’s internals far less mysterious.

Where the confusion starts

In numpy/_core/multiarray.py you’ll see core APIs decorated and wired up via an import that looks like a regular relative import. But there is no _multiarray_umath.py living next to it. Instead, NumPy installs a binary named _multiarray_umath.cpython-313-x86_64-linux-gnu.so under numpy/_core. That .so is the module actually imported by from . import _multiarray_umath.

Minimal code that reveals the binding

You can verify that Python is importing a binary shared module and see where it lives. The logic is straightforward: import the module and inspect its metadata.

import importlib
ua_handle = importlib.import_module('numpy._core._multiarray_umath')
print(type(ua_handle).__name__)
print(getattr(ua_handle, '__file__', 'no-file-attr'))

This illustrates that the import resolves to a shared module (.so) and shows the on-disk path inside numpy/_core.

What the .so actually is

The .so is a shared module compiled from roughly a hundred C and C++ sources spread across numpy/_core/src, including subdirectories like multiarray and umath. Some of those sources are generated C files, not hand-written ones. The build is defined and driven by NumPy’s Meson configuration.

How to see exactly how it’s built

The most direct way to understand the build is to build NumPy from source and then introspect the build graph using Meson. After configuring a build, you can emit a machine-readable description of top-level targets:

meson introspect --targets -i build/ > targets.json

Search in targets.json for the _multiarray_umath target. You’ll find an entry that identifies it as a shared module, where it’s defined, where it’s installed, and which sources and generated sources feed into it. A representative excerpt looks like this:

{
  "name": "_multiarray_umath.cpython-313-x86_64-linux-gnu",
  "type": "shared module",
  "defined_in": "/home/user/numpy/numpy/_core/meson.build",
  "filename": [
    "/home/user/numpy/build/numpy/_core/_multiarray_umath.cpython-313-x86_64-linux-gnu.so"
  ],
  "target_sources": [
    {
      "language": "c",
      "sources": [
        "/home/user/numpy/numpy/_core/src/multiarray/arrayobject.c",
        "/home/user/numpy/numpy/_core/src/multiarray/multiarraymodule.c",
        "/home/user/numpy/numpy/_core/src/umath/umathmodule.c"
      ],
      "generated_sources": [
        "/home/user/numpy/build/numpy/_core/_multiarray_umath.cpython-313-x86_64-linux-gnu.so.p/arraytypes.c",
        "/home/user/numpy/build/numpy/_core/_multiarray_umath.cpython-313-x86_64-linux-gnu.so.p/einsum.c"
      ]
    },
    {
      "language": "cpp",
      "sources": [
        "/home/user/numpy/numpy/_core/src/umath/clip.cpp",
        "/home/user/numpy/numpy/_core/src/umath/string_ufuncs.cpp"
      ]
    }
  ],
  "dependencies": ["openblas", "python-3.13"],
  "install_filename": [
    "/usr/lib/python3.13/site-packages/numpy/_core/_multiarray_umath.cpython-313-x86_64-linux-gnu.so"
  ]
}

This makes two things explicit. First, the module is defined in numpy/_core/meson.build. Second, along with hand-written C/C++ files, it includes generated C sources listed under generated_sources. These generated files commonly originate from .c.src templates. Meson’s introspection doesn’t surface the generation step details here.

Tracing sources in the tree

If you want to browse the relevant code, go straight to numpy/_core/src. That directory contains the C and C++ implementations that are compiled into the shared module you import. You’ll see multiarray and umath subtrees among others. While it’s easy to find Python shims like vstack in Python space, following concatenate or inner down into the C layer means reading these C/C++ sources and, in places, the generated C produced during the build.

Solution path: introspect and read the Meson definitions

The practical workflow is to build NumPy from source and introspect the build with Meson as shown above. Then, for a precise definition of how the _multiarray_umath module is composed, read the Meson rules that declare it. The module’s build description lives in NumPy’s Meson configuration and describes which sources and generated files are bundled together and how they are compiled. You can consult it here: numpy/_core/meson.build.

Why this matters for maintainers and power users

Understanding that NumPy’s core APIs are thin Python veneers over a large compiled shared module explains why you won’t find pure-Python function bodies for performance-critical operations. It also clarifies where to look when you need to audit behavior, debug low-level issues, reason about CPU feature flags, or study how generated code participates in the final binary. The Meson introspection output is a reliable map from the importable .so back to its sources, compiler flags, and link dependencies.

Wrap-up

If you can’t find a .py for a core NumPy function, assume it’s backed by a shared module in numpy/_core. Confirm the import and path in Python, then build from source and use meson introspect --targets -i build/ to enumerate the exact sources and generated sources that feed into _multiarray_umath. When in doubt, open numpy/_core/meson.build and follow the definitions. This approach keeps you grounded in what’s actually compiled and installed, and helps you navigate from high-level API to the underlying C and C++ implementations without guesswork.

The article is based on a question from StackOverflow by user90189 and an answer by Nick ODell.