Skip to content

[BUG] DLPackCompatible protocol members are declared as attributes, so np.ndarray fails to match — mx.array(np_arr) no longer type-checks #4371

Description

@dgrauet

Describe the bug

The DLPackCompatible protocol in the type stubs declares its members as mutable attributes rather than methods:

https://github.com/ml-explore/mlx/blob/main/python/mlx/_stub_patterns.txt#L5-L7

class DLPackCompatible(Protocol):
  __dlpack__: Callable[..., Any]
  __dlpack_device__: Callable[..., Any]

A protocol member declared as a variable is invariant and settable, so a class that defines __dlpack__ as a normal (read-only) method — like numpy.ndarray — does not match structurally. mypy states it plainly:

note: Protocol member DLPackCompatible.__dlpack__ expected settable variable, got read-only attribute

The result is that passing a NumPy array to any of the three entry points that accept DLPackCompatible fails type checking, even though all three work perfectly at runtime:

  • mx.array — python/src/array.cpp
  • mx.asarray — python/src/ops.cpp
  • mx.from_dlpack — python/src/ops.cpp

This is a regression against 0.31.2, where array.__init__ was annotated with numpy.ndarray directly:

# 0.31.2
def __init__(self: array, val: Union[scalar, list, tuple, numpy.ndarray, array], dtype: Optional[Dtype] = None): ...
# 0.32.1
def __init__(self: array, val: scalar | list | tuple | DLPackCompatible | array, dtype: Dtype | None = None): ...

(This is separate from #3916 — the stubs are back in 0.32.1, thanks. This is about their content.)

To Reproduce

# probe.py
import mlx.core as mx
import numpy as np

a: np.ndarray = np.zeros((2, 3), dtype=np.float32)
mx.array(a)
mx.asarray(a)
mx.from_dlpack(a)
uv venv --python 3.12 && uv pip install "mlx==0.32.1" numpy mypy
uv run mypy probe.py

Result on 0.32.1 (identical failures from ty):

probe.py:5: error: Argument 1 to "array" has incompatible type "ndarray[tuple[Any, ...], dtype[Any]]";
  expected "int | float | bool | complex | list[Any] | tuple[Any, ...] | DLPackCompatible | array"  [arg-type]
probe.py:6: error: Argument 1 to "asarray" has incompatible type ...  [arg-type]
probe.py:7: error: Argument 1 to "from_dlpack" has incompatible type "ndarray[...]";
  expected "DLPackCompatible"  [arg-type]
Found 3 errors in 1 file (checked 1 source file)

The same script on mlx==0.31.2:

Success: no issues found in 1 source file

And all three calls succeed at runtime on 0.32.1:

array    (2, 3)
asarray  (2, 3)
dlpack   (2, 3)

Expected behavior

mx.array(np_array) type-checks, as it did in 0.31.2. Runtime behaviour is already correct — only the stub is wrong.

Suggested fix

Declare the protocol members as methods, which makes them read-only and matched covariantly:

class DLPackCompatible(Protocol):
  def __dlpack__(self, *args: Any, **kwargs: Any) -> Any: ...
  def __dlpack_device__(self, *args: Any, **kwargs: Any) -> Any: ...

Verified: with that patch applied to the installed mlx/core/__init__.pyi, the repro above is clean under both mypy and ty (Success: no issues found / All checks passed!), on the same 0.32.1 wheel.

If you want a CI gate for this class of regression, the probe.py above is a good end-to-end canary — it exercises the stubs the way downstream projects actually do, and it would have caught both #3916 (stubs missing) and this one (stubs wrong).

Desktop (please complete the following information):

  • OS Version: macOS 15 (Darwin 25.5.0), Apple Silicon
  • Version: mlx 0.32.1 (mlx-0.32.1-cp312-cp312-macosx_15_0_arm64.whl), Python 3.12, mypy 2.3.1, ty 0.0.73

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions