Skip to content

Introduce mpl::array and mpl::defer - #32532

Open
iccir wants to merge 3 commits into
matplotlib:mainfrom
iccir:pybind-mplarray-initial
Open

iccir wants to merge 3 commits into
matplotlib:mainfrom
iccir:pybind-mplarray-initial

Conversation

@iccir

@iccir iccir commented Oct 23, 2026

Copy link
Copy Markdown
Contributor

PR summary

This pull request adds a new mpl::array C++ class as well as mpl::defer to mplutils.h. It changes the qhull module to use these instead of pybind11's py::array. Additional modules will be ported in the future.

ndarray Overview

nanobind provides an ndarray class for NumPy array access. While similar to pybind11's array class, it is not a drop-in replacement.

  • nb::ndarray does not automatically convert Python sequences.
  • nb::ndarray requires a const qualifier on scalar types in order to accept read-only arrays.
  • nb::ndarray has a different API. mutable_data() is simply data() on a non-const array. There is no unchecked() or unchecked_mutable(), etc.

nb::ndarray also provides shape constraints. In theory, these are great for code readability, as we can indicate an (N, 3) integer array as follows:

using TriangleArray = nb::ndarray<int, nb::shape<-1, 3>>;

Unfortunately, nanobind is exceedingly vague when it fails to match a type signature to a bound function. When porting modules, you often end up with:

TypeError: foo(): incompatible function arguments.
The following argument types are supported:
1. foo(x: ndarray[dtype=uint8, order='C']) -> tuple

Invoked with types: ndarray

The root cause can be your scalar is int instead of const int. Or it can be that the shape constraint is mismatched. You don't know. This is especially fun when your function takes 5+ different ndarray types and nanobind doesn't indicate which one caused the failure.

nanobind also requires that all typecasters be noexcept and return a simple false in the event of an error. Hence, there is no way to provide detailed information on why the type caster failed.

Typical Recommendation

The typical recommendation to the above problem is: "don't use a type caster, declare your function to take an nb::object and then convert inside the function body":

static nb::tuple foo(nb::object src_object)
{
    nb::ndarray<double, nb::device::cpu, nb::c_contig> foo_arr;

    if (nb::try_cast(src_object, rect_arr)) {
        …
    }

    …
}

This is not ideal:

  1. The function's signature is vague and results in vague types with nanobind's stubgen tool.
  2. You fail if src_object is a Python sequence.
  3. nb::try_cast returns a boolean. nb::cast throws a vague error message.

My Solution

My solution involves two parts: a simple wrapper which defers object creation combined with a modified array class which provides detailed error messages.

mpl::defer

mpl::defer is a simple pybind or nanobind wrapper class which defers the creation of an object until value() is called:

template <typename T>
class defer : public py::object // (or nb::object)
{
    … // PYBIND11_OBJECT_DEFAULT or NB_OBJECT_DEFAULT
    template <typename... Args>
    T value(Args &&...args) { return T(*this, args...); }
};

This is used in a bound function as follows:

static nb::tuple foo(const mpl::defer<DeferredObject> &src)
{
    auto actual = src.value();
    …
}

mpl::array

mpl::array is effectively a vendored version of nb::ndarray from nanobind's "ndarray.h". It automatically handles Python sequences and provides detailed error messages when a conversion fails. It also allows for "unmanaged" variants which are not backed by a NumPy array.

It will eventually replace several of our utility functions and classes:

Old New
safe_first_shape() mpl::array::has_members()
check_trailing_shape() mpl::shape / mpl::ndim constraints
class empty (array.h) mpl::array::unmanaged_empty()
class scalar (array.h) mpl::array::unmanaged_scalar()
Manually comparing shape of arrays mpl::array::is_same_shape()

In this PR, it wraps a py::array and lives in "py_array.h". In a future PR, there will be a variant that wraps an nb::ndarray and lives in "mplarray.h". Both variants can exist as we adopt nanobind.

Note

For simplicity, an mpl::array always has an associated data type and shape. We have a few situations that use data-type-less or shapeless arrays. Those situations will continue to directly use py::array or nb::ndarray.

Qhull changes

The changes to qhull are straightforward:

  • Two py::array types change to mpl::array.
  • mutable_data() becomes data().
  • An ndim() check is removed as the mpl::array constructor handles this for us.

C++ Disclosure

The creation of C++ container classes and heavy template metaprogramming is not something that I do everyday. It involves copious amounts of coffee, epic video game battle music, and intense focus.

Please scrutinize accordingly. While most of the heavy lifting is directly ported from nanobind, I am still filling gaps in my C++ knowledge.

AI Disclosure

  • I use AI for web search due to search engines becoming less reliable.
  • A lot of code was adapted from my nanobind experiment where I used AI to help update my C++ knowledge to C++17.
  • I used AI to review my implementation of py_array.h and check it against nanobind's ndarray class.

PR quality check

  • Use an expressive title, e.g. "Fix title font property precedence"
  • New and changed code is tested
  • [/] Plotting related features are demonstrated in an example
  • [/] New features and API changes have release notes
  • [/] Documentation complies with general and docstring guidelines

@iccir

iccir commented Oct 23, 2026

Copy link
Copy Markdown
Contributor Author

I'm going to go ahead and mark this as "Ready for review". I believe that the Windows test failure is due to a timeout in the test and AppVeyor didn't make it to the finish line in time.

@iccir
iccir marked this pull request as ready for review October 23, 2026 16:23
@iccir iccir mentioned this pull request Oct 24, 2026
2 tasks done

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant