diff --git a/Lib/test/test_array.py b/Lib/test/test_array.py index b5f6603defde5c..85ce651198c0f6 100755 --- a/Lib/test/test_array.py +++ b/Lib/test/test_array.py @@ -209,6 +209,10 @@ def test_numbers(self): [-1<<63, (1<<63)-1, 0]), (['l'], SIGNED_INT64_BE, '>qqq', [-1<<63, (1<<63)-1, 0]), + (['e'], IEEE_754_FLOAT16_LE, 'eeee', + [1.0, float('inf'), float('-inf'), -0.0]), (['f'], IEEE_754_FLOAT_LE, 'ffff', @@ -239,6 +243,28 @@ def test_numbers(self): self.assertEqual(a, b, msg="{0!r} != {1!r}; testcase={2!r}".format(a, b, testcase)) + def test_float16_endianness(self): + # gh-issue: the slow-path decoder for IEEE_754_FLOAT16_LE/BE + # compared mformat_code against the 32-bit float constant + # (IEEE_754_FLOAT_LE) instead of the float16 one, so it always + # decoded as big-endian regardless of what was requested. + # + # A real 'e'-typecode array only exercises the slow (converting) + # path when the requested mformat disagrees with the *native* + # float16 format, so which of LE/BE actually exercises the buggy + # branch depends on the test machine's endianness, and the other + # direction happens to come out "correct by coincidence" because + # the bug unconditionally decodes as big-endian. Using a + # typecode ('d') whose native mformat can never match + # IEEE_754_FLOAT16_LE/BE forces the slow path deterministically + # on any machine, so both directions are actually exercised. + le_bytes = struct.pack('e', 1.0) + b_le = array_reconstructor(array.array, 'd', IEEE_754_FLOAT16_LE, le_bytes) + b_be = array_reconstructor(array.array, 'd', IEEE_754_FLOAT16_BE, be_bytes) + self.assertEqual(b_le.tolist(), [1.0]) + self.assertEqual(b_be.tolist(), [1.0]) + def test_unicode(self): teststr = "Bonne Journ\xe9e \U0002030a\U00020347" testcases = ( diff --git a/Misc/NEWS.d/next/Library/2026-07-23-21-00-00.gh-issue-154568.float16-reconstructor.rst b/Misc/NEWS.d/next/Library/2026-07-23-21-00-00.gh-issue-154568.float16-reconstructor.rst new file mode 100644 index 00000000000000..00c33d8073daf8 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-23-21-00-00.gh-issue-154568.float16-reconstructor.rst @@ -0,0 +1,5 @@ +Fix ``array._array_reconstructor`` (used internally for unpickling +:class:`array.array`) ignoring the requested byte order and always +decoded as big-endian. This could silently produce wrong values when +unpickling a half precision float array on a machine with different +native endianness than the one that pickled it. diff --git a/Modules/arraymodule.c b/Modules/arraymodule.c index 68486c66575933..39a399d7a49cf5 100644 --- a/Modules/arraymodule.c +++ b/Modules/arraymodule.c @@ -2281,7 +2281,7 @@ array__array_reconstructor_impl(PyObject *module, PyTypeObject *arraytype, case IEEE_754_FLOAT16_LE: case IEEE_754_FLOAT16_BE: { Py_ssize_t i; - int le = (mformat_code == IEEE_754_FLOAT_LE) ? 1 : 0; + int le = (mformat_code == IEEE_754_FLOAT16_LE) ? 1 : 0; Py_ssize_t itemcount = Py_SIZE(items) / 2; const char *memstr = PyBytes_AS_STRING(items);