Skip to content

Reinstate __array__ on Python 3.10/3.11 #162

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions .github/workflows/array-api-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,16 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.12', '3.13']
numpy-version: ['1.26', '2.2', 'dev']
python-version: ['3.10', '3.11', '3.12', '3.13']
numpy-version: ['1.26', '2.3', 'dev']
exclude:
- python-version: '3.10'
numpy-version: '2.3'
- python-version: '3.10'
numpy-version: 'dev'
- python-version: '3.13'
numpy-version: '1.26'

fail-fast: false
steps:
- name: Checkout array-api-strict
uses: actions/checkout@v4
Expand Down
8 changes: 6 additions & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,15 @@ jobs:
strategy:
matrix:
python-version: ['3.10', '3.11', '3.12', '3.13']
numpy-version: ['1.26', 'dev']
numpy-version: ['1.26', '2.3', 'dev']
exclude:
- python-version: '3.10'
numpy-version: '2.3'
- python-version: '3.10'
numpy-version: 'dev'
- python-version: '3.13'
numpy-version: '1.26'
fail-fast: true
fail-fast: false
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
Expand Down
46 changes: 28 additions & 18 deletions array_api_strict/_array_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from __future__ import annotations

import operator
import sys
from collections.abc import Iterator
from enum import IntEnum
from types import EllipsisType, ModuleType
Expand Down Expand Up @@ -67,8 +68,6 @@ def __hash__(self) -> int:
CPU_DEVICE = Device()
ALL_DEVICES = (CPU_DEVICE, Device("device1"), Device("device2"))

_default = object()


class Array:
"""
Expand Down Expand Up @@ -149,29 +148,40 @@ def __repr__(self) -> str:

__str__ = __repr__

# `__array__` was implemented historically for compatibility, and removing it has
# caused issues for some libraries (see
# https://github.com/data-apis/array-api-strict/issues/67).

# Instead of `__array__` we now implement the buffer protocol.
# Note that it makes array-apis-strict requiring python>=3.12
def __buffer__(self, flags):
if self._device != CPU_DEVICE:
raise RuntimeError(f"Can not convert array on the '{self._device}' device to a Numpy array.")
raise RuntimeError(
# NumPy swallows this exception and falls back to __array__.
f"Can't extract host buffer from array on the '{self._device}' device."
)
return self._array.__buffer__(flags)

# We do not define __release_buffer__, per the discussion at
# https://github.com/data-apis/array-api-strict/pull/115#pullrequestreview-2917178729

def __array__(self, *args, **kwds):
# a stub for python < 3.12; otherwise numpy silently produces object arrays
import sys
minor, major = sys.version_info.minor, sys.version_info.major
if major < 3 or minor < 12:
# `__array__` is not part of the Array API. Ideally we want to support
# `xp.asarray(Array)` exclusively through the __buffer__ protocol; however this is
# only possible on Python >=3.12. Additionally, when __buffer__ raises (e.g. because
# the array is not on the CPU device, NumPy will try to fall back on __array__ but,
# if that doesn't exist, create a scalar numpy array of objects which contains the
# array_api_strict.Array. So we can't get rid of __array__ entirely.
def __array__(
self, dtype: None | np.dtype[Any] = None, copy: None | bool = None
) -> npt.NDArray[Any]:
if self._device != CPU_DEVICE:
# We arrive here from np.asarray() on Python >=3.12 when __buffer__ raises.
raise RuntimeError(
f"Can't convert array on the '{self._device}' device to a "
"NumPy array."
)
Comment on lines +169 to +173
Copy link
Contributor Author

@crusaderky crusaderky Jul 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previously, np.asarray() would raise ValueError: object __array__ method not producing an array because __buffer__ raised and __array__ subtly returned None. Changed to explicit behaviour.

if sys.version_info >= (3, 12):
raise TypeError(
"Interoperation with NumPy requires python >= 3.12. Please upgrade."
"The __array__ method is not supported by the Array API. "
"Please use the __buffer__ interface instead."
)

# copy keyword is new in 2.0
if np.__version__[0] < '2':
return np.asarray(self._array, dtype=dtype)
return np.asarray(self._array, dtype=dtype, copy=copy)

# These are various helper functions to make the array behavior match the
# spec in places where it either deviates from or is more strict than
# NumPy behavior
Expand Down
54 changes: 36 additions & 18 deletions array_api_strict/tests/test_array_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -559,36 +559,54 @@ def test_array_properties():
assert b.mT.shape == (3, 2)


@pytest.mark.xfail(sys.version_info.major*100 + sys.version_info.minor < 312,
reason="array conversion relies on buffer protocol, and "
"requires python >= 3.12"
)
def test_array_conversion():
# Check that arrays on the CPU device can be converted to NumPy
# but arrays on other devices can't. Note this is testing the logic in
# __array__, which is only used in asarray when converting lists of
# arrays.
# __array__ on Python 3.10~3.11 and in __buffer__ on Python >=3.12.
a = ones((2, 3))
np.asarray(a)
na = np.asarray(a)
assert na.shape == (2, 3)
assert na.dtype == np.float64
na[0, 0] = 10
assert a[0, 0] == 10 # return view when possible

a = arange(5, dtype=uint8)
na = np.asarray(a)
assert na.dtype == np.uint8

for device in ("device1", "device2"):
a = ones((2, 3), device=array_api_strict.Device(device))
with pytest.raises((RuntimeError, ValueError)):
with pytest.raises(RuntimeError, match=device):
np.asarray(a)

# __buffer__ should work for now for conversion to numpy
a = ones((2, 3))
na = np.array(a)
assert na.shape == (2, 3)
assert na.dtype == np.float64

@pytest.mark.skipif(not sys.version_info.major*100 + sys.version_info.minor < 312,
reason="conversion to numpy errors out unless python >= 3.12"
@pytest.mark.skipif(np.__version__ < "2", reason="np.asarray has no copy kwarg")
def test_array_conversion_copy():
a = arange(5)
na = np.asarray(a, copy=False)
na[0] = 10
assert a[0] == 10

a = arange(5)
na = np.asarray(a, copy=True)
na[0] = 10
assert a[0] == 0

a = arange(5)
with pytest.raises(ValueError):
np.asarray(a, dtype=np.uint8, copy=False)


@pytest.mark.skipif(
sys.version_info < (3, 12), reason="Python <3.12 has no __buffer__ interface"
)
def test_array_conversion_2():
def test_no_array_interface():
"""When the __buffer__ interface is available, the __array__ interface is not."""
a = ones((2, 3))
with pytest.raises(TypeError):
np.array(a)
with pytest.raises(TypeError, match="not supported"):
# Because NumPy prefers __buffer__ when available, we can't trigger this
# exception from np.asarray().
a.__array__()


def test_allow_newaxis():
Expand Down
25 changes: 25 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,30 @@
# Changelog

## 2.4.1 (unreleased)

### Major Changes

- The array object defines `__array__` again when `__buffer__` is not available.

- Support for Python versions 3.10 and 3.11 has been reinstated.


### Minor Changes

- Arithmetic operations no longer accept NumPy arrays.

- Disallow `__setitem__` for invalid dtype combinations (e.g. setting a float value
into an integer array)


### Contributors

The following users contributed to this release:

Evgeni Burovski,
Guido Imperiale


## 2.4.0 (2025-06-16)

### Major Changes
Expand Down
Loading