From 30d982280490c70ca22301dbe9faf5595981ba1d Mon Sep 17 00:00:00 2001 From: PhysicistJohn <54456354+PhysicistJohn@users.noreply.github.com> Date: Thu, 23 Jul 2026 19:50:35 -0700 Subject: [PATCH 1/3] gh-154568: Fix array._array_reconstructor() ignoring requested byte order for float16 The slow-path decoder for IEEE_754_FLOAT16_LE/BE computed the byte-order flag by comparing mformat_code against IEEE_754_FLOAT_LE (the 32-bit float constant) instead of IEEE_754_FLOAT16_LE. Since the float16 mformat codes are never equal to that constant, the comparison was always false, so the decoder always treated input as big-endian regardless of what was requested. Adds a regression test. A real 'e'-typecode array only exercises the slow/converting path when the requested mformat disagrees with the machine's native float16 format, so which direction (LE/BE) exercises the buggy branch depends on the test machine's endianness, and the other direction happens to come out "correct" by coincidence since the bug unconditionally decodes as big-endian. The test uses a typecode ('d') whose native format can never match the float16 codes, forcing the slow path deterministically on any machine. --- Lib/test/test_array.py | 22 +++++++++++++++++++ ....gh-issue-154568.float16-reconstructor.rst | 8 +++++++ Modules/arraymodule.c | 2 +- 3 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2026-07-23-21-00-00.gh-issue-154568.float16-reconstructor.rst diff --git a/Lib/test/test_array.py b/Lib/test/test_array.py index b5f6603defde5c5..ee6ed1c1262aa34 100755 --- a/Lib/test/test_array.py +++ b/Lib/test/test_array.py @@ -239,6 +239,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 000000000000000..475b9e591a7fc95 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-23-21-00-00.gh-issue-154568.float16-reconstructor.rst @@ -0,0 +1,8 @@ +Fix :func:`array._array_reconstructor` (used internally for unpickling +:class:`array.array`) ignoring the requested byte order for the +``IEEE_754_FLOAT16_LE``/``IEEE_754_FLOAT16_BE`` machine format codes: it +compared against the 32-bit float constant instead of the float16 one, so +it always decoded as big-endian regardless of what was requested. This +could silently produce wrong values when unpickling an ``'e'``-typecode +(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 68486c66575933a..39a399d7a49cf54 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); From fc94566aa5129301ce77e6d0f8115432054c8d8b Mon Sep 17 00:00:00 2001 From: PhysicistJohn <54456354+PhysicistJohn@users.noreply.github.com> Date: Thu, 23 Jul 2026 19:56:15 -0700 Subject: [PATCH 2/3] Fix NEWS entry: don't use :func: role for an undocumented private function array._array_reconstructor is internal/private and has no Sphinx docs entry, so the :func: cross-reference role can't resolve and trips the docs CI's new-NEWS-nit check. --- ...026-07-23-21-00-00.gh-issue-154568.float16-reconstructor.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 475b9e591a7fc95..d1a8763571cb082 100644 --- 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 @@ -1,4 +1,4 @@ -Fix :func:`array._array_reconstructor` (used internally for unpickling +Fix ``array._array_reconstructor`` (used internally for unpickling :class:`array.array`) ignoring the requested byte order for the ``IEEE_754_FLOAT16_LE``/``IEEE_754_FLOAT16_BE`` machine format codes: it compared against the 32-bit float constant instead of the float16 one, so From e8631d122749bb4bead081c562bd535a7b49c29c Mon Sep 17 00:00:00 2001 From: PhysicistJohn <54456354+PhysicistJohn@users.noreply.github.com> Date: Thu, 23 Jul 2026 22:50:07 -0700 Subject: [PATCH 3/3] Address review feedback: add half-floats case to test_numbers(), simplify NEWS wording Per skirpichev's review on GH-154569: - Add a float16 (LE/BE) case to ArrayReconstructorTest.test_numbers() for basic functional coverage alongside the other numeric types, complementing the existing dedicated test_float16_endianness (which he confirmed is fine to keep for deterministically forcing the buggy slow path regardless of test-machine endianness). - Simplify the NEWS entry wording per his suggested rewrite. --- Lib/test/test_array.py | 4 ++++ ...21-00-00.gh-issue-154568.float16-reconstructor.rst | 11 ++++------- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/Lib/test/test_array.py b/Lib/test/test_array.py index ee6ed1c1262aa34..85ce651198c0f6a 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', 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 index d1a8763571cb082..00c33d8073daf87 100644 --- 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 @@ -1,8 +1,5 @@ Fix ``array._array_reconstructor`` (used internally for unpickling -:class:`array.array`) ignoring the requested byte order for the -``IEEE_754_FLOAT16_LE``/``IEEE_754_FLOAT16_BE`` machine format codes: it -compared against the 32-bit float constant instead of the float16 one, so -it always decoded as big-endian regardless of what was requested. This -could silently produce wrong values when unpickling an ``'e'``-typecode -(half precision float) array on a machine with different native -endianness than the one that pickled it. +: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.