From 1afb3b9701ceb56f5f5c68c224daab0c88511d19 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Sat, 21 Mar 2026 06:06:23 +0300 Subject: [PATCH 1/5] gh-146151: support for complex arrays in the array module --- Doc/library/array.rst | 21 +- Doc/whatsnew/3.15.rst | 9 + Lib/test/test_array.py | 68 +++++- ...-03-21-06-21-38.gh-issue-146151.yNpgml.rst | 3 + Modules/arraymodule.c | 199 ++++++++++++++++-- 5 files changed, 280 insertions(+), 20 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-03-21-06-21-38.gh-issue-146151.yNpgml.rst diff --git a/Doc/library/array.rst b/Doc/library/array.rst index 5592bd7089ba49..dadae6ff71a56a 100644 --- a/Doc/library/array.rst +++ b/Doc/library/array.rst @@ -9,7 +9,7 @@ -------------- This module defines an object type which can compactly represent an array of -basic values: characters, integers, floating-point numbers. Arrays are mutable :term:`sequence` +basic values: characters, integers, floating-point numbers, complex numbers. Arrays are mutable :term:`sequence` types and behave very much like lists, except that the type of objects stored in them is constrained. The type is specified at object creation time by using a :dfn:`type code`, which is a single character. The following type codes are @@ -46,6 +46,11 @@ defined: +-----------+--------------------+-------------------+-----------------------+-------+ | ``'d'`` | double | float | 8 | | +-----------+--------------------+-------------------+-----------------------+-------+ +| ``'F'`` | float complex | complex | 8 | \(3) | ++-----------+--------------------+-------------------+-----------------------+-------+ +| ``'D'`` | double complex | complex | 16 | \(3) | ++-----------+--------------------+-------------------+-----------------------+-------+ + Notes: @@ -63,6 +68,15 @@ Notes: (2) .. versionadded:: 3.13 +(3) + Complex types (``F`` and ``D``) are available unconditionally, + regardless on support for complex types (the Annex G of the C11 standard) + by the C compiler. + As specified in the C11 standard, each complex type is represented by a + two-element C array containing, respectively, the real and imaginary parts. + + .. versionadded:: 3.15 + The actual representation of values is determined by the machine architecture (strictly speaking, by the C implementation). The actual size can be accessed @@ -139,9 +153,10 @@ The module defines the following type: .. method:: byteswap() "Byteswap" all items of the array. This is only supported for values which are - 1, 2, 4, or 8 bytes in size; for other types of values, :exc:`RuntimeError` is + 1, 2, 4, 8 or 16 bytes in size; for other types of values, :exc:`RuntimeError` is raised. It is useful when reading data from a file written on a machine with a - different byte order. + different byte order. Note, that for complex types the order of + components (the real part, followed by imaginary part) is preserved. .. method:: count(x) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 5689ecbffc4b30..2199dc2e4118b3 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -629,6 +629,15 @@ argparse (Contributed by Savannah Ostrowski in :gh:`142390`.) +array +----- + +* Support the :c:expr:`float complex` and :c:expr:`double complex` C types in + the :mod:`array` module (formatting characters ``'F'`` and ``'D'`` + respectively). + (Contributed by Sergey B Kirpichev in :gh:`146151`.) + + base64 ------ diff --git a/Lib/test/test_array.py b/Lib/test/test_array.py index 5c919aea24ed94..24148917f103fe 100755 --- a/Lib/test/test_array.py +++ b/Lib/test/test_array.py @@ -31,7 +31,7 @@ class ArraySubclassWithKwargs(array.array): def __init__(self, typecode, newarg=None): array.array.__init__(self) -typecodes = 'uwbBhHiIlLfdqQ' +typecodes = 'uwbBhHiIlLfdqQFD' class MiscTest(unittest.TestCase): @@ -113,6 +113,10 @@ def __index__(self): UTF16_BE = 19 UTF32_LE = 20 UTF32_BE = 21 +IEEE_754_FLOAT_COMPLEX_LE = 22 +IEEE_754_FLOAT_COMPLEX_BE = 23 +IEEE_754_DOUBLE_COMPLEX_LE = 24 +IEEE_754_DOUBLE_COMPLEX_BE = 25 class ArrayReconstructorTest(unittest.TestCase): @@ -139,7 +143,7 @@ def test_error(self): self.assertRaises(ValueError, array_reconstructor, array.array, "b", UNKNOWN_FORMAT, b"") self.assertRaises(ValueError, array_reconstructor, - array.array, "b", 22, b"") + array.array, "b", 26, b"") self.assertRaises(ValueError, array_reconstructor, array.array, "d", 16, b"a") @@ -279,7 +283,7 @@ def test_byteswap(self): example = self.example a = array.array(self.typecode, example) self.assertRaises(TypeError, a.byteswap, 42) - if a.itemsize in (1, 2, 4, 8): + if a.itemsize in (1, 2, 4, 8, 16): b = array.array(self.typecode, example) b.byteswap() if a.itemsize==1: @@ -1525,6 +1529,55 @@ def test_byteswap(self): b.byteswap() self.assertEqual(a, b) +class CFPTest(NumberTest): + example = [-42j, 0, 42+1j, 1e5j, -1e10] + outside = 23 + + def assertEntryEqual(self, entry1, entry2): + self.assertAlmostEqual(entry1, entry2) + + def test_cmp(self): + a = array.array(self.typecode, self.example) + self.assertIs(a == 42, False) + self.assertIs(a != 42, True) + + self.assertIs(a == a, True) + self.assertIs(a != a, False) + self.assertIs(a < a, False) + self.assertIs(a <= a, True) + self.assertIs(a > a, False) + self.assertIs(a >= a, True) + + self.assertIs(a == 2*a, False) + self.assertIs(a != 2*a, True) + self.assertIs(a < 2*a, True) + self.assertIs(a <= 2*a, True) + self.assertIs(a > 2*a, False) + self.assertIs(a >= 2*a, False) + + def test_nan(self): + a = array.array(self.typecode, [float('nan')]) + b = array.array(self.typecode, [float('nan')]) + self.assertIs(a != b, True) + self.assertIs(a == b, False) + + def test_byteswap(self): + a = array.array(self.typecode, self.example) + self.assertRaises(TypeError, a.byteswap, 42) + if a.itemsize in (1, 2, 4, 8): + b = array.array(self.typecode, self.example) + b.byteswap() + if a.itemsize==1: + self.assertEqual(a, b) + else: + # On alphas treating the byte swapped bit patterns as + # floats/doubles results in floating-point exceptions + # => compare the 8bit string values instead + self.assertNotEqual(a.tobytes(), b.tobytes()) + b.byteswap() + self.assertEqual(a, b) + + class FloatTest(FPTest, unittest.TestCase): typecode = 'f' minitemsize = 4 @@ -1551,6 +1604,15 @@ def test_alloc_overflow(self): self.fail("Array of size > maxsize created - MemoryError expected") +class ComplexFloatTest(CFPTest, unittest.TestCase): + typecode = 'F' + minitemsize = 8 + +class ComplexDoubleTest(CFPTest, unittest.TestCase): + typecode = 'D' + minitemsize = 16 + + class LargeArrayTest(unittest.TestCase): typecode = 'b' diff --git a/Misc/NEWS.d/next/Library/2026-03-21-06-21-38.gh-issue-146151.yNpgml.rst b/Misc/NEWS.d/next/Library/2026-03-21-06-21-38.gh-issue-146151.yNpgml.rst new file mode 100644 index 00000000000000..14b179fc1cb9e9 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-03-21-06-21-38.gh-issue-146151.yNpgml.rst @@ -0,0 +1,3 @@ +Support the :c:expr:`float complex` and :c:expr:`double complex` C types in +the :mod:`array` module (formatting characters ``'F'`` and ``'D'`` +respectively). Patch by Sergey B Kirpichev. diff --git a/Modules/arraymodule.c b/Modules/arraymodule.c index ec6a9840131e4d..661af86f483363 100644 --- a/Modules/arraymodule.c +++ b/Modules/arraymodule.c @@ -11,6 +11,7 @@ #include "pycore_bytesobject.h" // _PyBytes_Repeat #include "pycore_call.h" // _PyObject_CallMethod() #include "pycore_ceval.h" // _PyEval_GetBuiltin() +#include "pycore_floatobject.h" // _PY_FLOAT_BIG_ENDIAN #include "pycore_modsupport.h" // _PyArg_NoKeywords() #include "pycore_moduleobject.h" // _PyModule_GetState() #include "pycore_weakref.h" // FT_CLEAR_WEAKREFS() @@ -117,10 +118,14 @@ enum machine_format_code { UTF16_LE = 18, UTF16_BE = 19, UTF32_LE = 20, - UTF32_BE = 21 + UTF32_BE = 21, + IEEE_754_FLOAT_COMPLEX_LE = 22, + IEEE_754_FLOAT_COMPLEX_BE = 23, + IEEE_754_DOUBLE_COMPLEX_LE = 24, + IEEE_754_DOUBLE_COMPLEX_BE = 25 }; #define MACHINE_FORMAT_CODE_MIN 0 -#define MACHINE_FORMAT_CODE_MAX 21 +#define MACHINE_FORMAT_CODE_MAX 25 /* @@ -649,6 +654,64 @@ d_setitem(arrayobject *ap, Py_ssize_t i, PyObject *v) return 0; } +static PyObject * +cf_getitem(arrayobject *ap, Py_ssize_t i) +{ + float f[2]; + + memcpy(&f, ap->ob_item + i*sizeof(f), sizeof(f)); + return PyComplex_FromDoubles(f[0], f[1]); +} + +static int +cf_setitem(arrayobject *ap, Py_ssize_t i, PyObject *v) +{ + Py_complex x; + float f[2]; + + if (!PyArg_Parse(v, "D;array item must be complex", &x)) { + return -1; + } + + CHECK_ARRAY_BOUNDS(ap, i); + + f[0] = x.real; + f[1] = x.imag; + if (i >= 0) { + memcpy(ap->ob_item + i*sizeof(f), &f, sizeof(f)); + } + return 0; +} + +static PyObject * +cd_getitem(arrayobject *ap, Py_ssize_t i) +{ + double f[2]; + + memcpy(&f, ap->ob_item + i*sizeof(f), sizeof(f)); + return PyComplex_FromDoubles(f[0], f[1]); +} + +static int +cd_setitem(arrayobject *ap, Py_ssize_t i, PyObject *v) +{ + Py_complex x; + double f[2]; + + if (!PyArg_Parse(v, "D;array item must be complex", &x)) { + return -1; + } + + CHECK_ARRAY_BOUNDS(ap, i); + + f[0] = x.real; + f[1] = x.imag; + if (i >= 0) { + memcpy(ap->ob_item + i*sizeof(f), &f, sizeof(f)); + } + return 0; +} + #define DEFINE_COMPAREITEMS(code, type) \ static int \ code##_compareitems(const void *lhs, const void *rhs, Py_ssize_t length) \ @@ -693,6 +756,8 @@ static const struct arraydescr descriptors[] = { {'Q', sizeof(long long), QQ_getitem, QQ_setitem, QQ_compareitems, "Q", 1, 0}, {'f', sizeof(float), f_getitem, f_setitem, NULL, "f", 0, 0}, {'d', sizeof(double), d_getitem, d_setitem, NULL, "d", 0, 0}, + {'F', 2*sizeof(float), cf_getitem, cf_setitem, NULL, "F", 0, 0}, + {'D', 2*sizeof(double), cd_getitem, cd_setitem, NULL, "D", 0, 0}, {'\0', 0, 0, 0, 0, 0, 0} /* Sentinel */ }; @@ -1496,8 +1561,9 @@ array.array.byteswap Byteswap all items of the array. -If the items in the array are not 1, 2, 4, or 8 bytes in size, RuntimeError is -raised. +If the items in the array are not 1, 2, 4, 8 or 16 bytes in size, RuntimeError is +raised. Note, that for complex types the order of +components (the real part, followed by imaginary part) is preserved. [clinic start generated code]*/ static PyObject * @@ -1528,19 +1594,66 @@ array_array_byteswap_impl(arrayobject *self) } break; case 8: + if (self->ob_descr->typecode != 'F') { + for (p = self->ob_item, i = Py_SIZE(self); --i >= 0; p += 8) { + char p0 = p[0]; + char p1 = p[1]; + char p2 = p[2]; + char p3 = p[3]; + p[0] = p[7]; + p[1] = p[6]; + p[2] = p[5]; + p[3] = p[4]; + p[4] = p3; + p[5] = p2; + p[6] = p1; + p[7] = p0; + } + } + else { + for (p = self->ob_item, i = Py_SIZE(self); --i >= 0; p += 8) { + char t0 = p[0]; + char t1 = p[1]; + p[0] = p[3]; + p[1] = p[2]; + p[2] = t1; + p[3] = t0; + t0 = p[4]; + t1 = p[5]; + p[4] = p[7]; + p[5] = p[6]; + p[6] = t1; + p[7] = t0; + } + } + break; + case 16: + assert(self->ob_descr->typecode == 'D'); for (p = self->ob_item, i = Py_SIZE(self); --i >= 0; p += 8) { - char p0 = p[0]; - char p1 = p[1]; - char p2 = p[2]; - char p3 = p[3]; + char t0 = p[0]; + char t1 = p[1]; + char t2 = p[2]; + char t3 = p[3]; p[0] = p[7]; p[1] = p[6]; p[2] = p[5]; p[3] = p[4]; - p[4] = p3; - p[5] = p2; - p[6] = p1; - p[7] = p0; + p[4] = t3; + p[5] = t2; + p[6] = t1; + p[7] = t0; + t0 = p[8]; + t1 = p[9]; + t2 = p[10]; + t3 = p[11]; + p[8] = p[15]; + p[9] = p[14]; + p[10] = p[13]; + p[11] = p[12]; + p[12] = t3; + p[13] = t2; + p[14] = t1; + p[15] = t0; } break; default: @@ -1975,7 +2088,11 @@ static const struct mformatdescr { {4, 0, 0}, /* 18: UTF16_LE */ {4, 0, 1}, /* 19: UTF16_BE */ {8, 0, 0}, /* 20: UTF32_LE */ - {8, 0, 1} /* 21: UTF32_BE */ + {8, 0, 1}, /* 21: UTF32_BE */ + {8, 0, 0}, /* 22: IEEE_754_FLOAT_COMPLEX_LE */ + {8, 0, 1}, /* 23: IEEE_754_FLOAT_COMPLEX_BE */ + {16, 0, 0}, /* 24: IEEE_754_DOUBLE_COMPLEX_LE */ + {16, 0, 1}, /* 25: IEEE_754_DOUBLE_COMPLEX_BE */ }; @@ -2030,6 +2147,14 @@ typecode_to_mformat_code(char typecode) } return UNKNOWN_FORMAT; + case 'F': + return _PY_FLOAT_BIG_ENDIAN ? \ + IEEE_754_FLOAT_COMPLEX_BE : IEEE_754_FLOAT_COMPLEX_LE; + + case 'D': + return _PY_FLOAT_BIG_ENDIAN ? \ + IEEE_754_DOUBLE_COMPLEX_BE : IEEE_754_DOUBLE_COMPLEX_LE; + /* Integers */ case 'h': intsize = sizeof(short); @@ -2243,6 +2368,50 @@ array__array_reconstructor_impl(PyObject *module, PyTypeObject *arraytype, } break; } + case IEEE_754_FLOAT_COMPLEX_LE: + case IEEE_754_FLOAT_COMPLEX_BE: { + Py_ssize_t i; + int le = (mformat_code == IEEE_754_FLOAT_COMPLEX_LE) ? 1 : 0; + Py_ssize_t itemcount = Py_SIZE(items) / 8; + const char *memstr = PyBytes_AS_STRING(items); + + converted_items = PyList_New(itemcount); + if (converted_items == NULL) + return NULL; + for (i = 0; i < itemcount; i++) { + PyObject *pycomplex = PyComplex_FromDoubles( + PyFloat_Unpack4(&memstr[i * 8], le), + PyFloat_Unpack4(&memstr[i * 8] + 4, le)); + if (pycomplex == NULL) { + Py_DECREF(converted_items); + return NULL; + } + PyList_SET_ITEM(converted_items, i, pycomplex); + } + break; + } + case IEEE_754_DOUBLE_COMPLEX_LE: + case IEEE_754_DOUBLE_COMPLEX_BE: { + Py_ssize_t i; + int le = (mformat_code == IEEE_754_DOUBLE_COMPLEX_LE) ? 1 : 0; + Py_ssize_t itemcount = Py_SIZE(items) / 16; + const char *memstr = PyBytes_AS_STRING(items); + + converted_items = PyList_New(itemcount); + if (converted_items == NULL) + return NULL; + for (i = 0; i < itemcount; i++) { + PyObject *pycomplex = PyComplex_FromDoubles( + PyFloat_Unpack8(&memstr[i * 16], le), + PyFloat_Unpack8(&memstr[i * 16] + 8, le)); + if (pycomplex == NULL) { + Py_DECREF(converted_items); + return NULL; + } + PyList_SET_ITEM(converted_items, i, pycomplex); + } + break; + } case UTF16_LE: case UTF16_BE: { int byteorder = (mformat_code == UTF16_LE) ? -1 : 1; @@ -2950,7 +3119,7 @@ array_new(PyTypeObject *type, PyObject *args, PyObject *kwds) PyDoc_STRVAR(module_doc, "This module defines an object type which can efficiently represent\n\ an array of basic values: characters, integers, floating-point\n\ -numbers. Arrays are sequence types and behave very much like lists,\n\ +numbers, complex numbers. Arrays are sequence types and behave very much like lists,\n\ except that the type of objects stored in them is constrained.\n"); PyDoc_STRVAR(arraytype_doc, @@ -2979,6 +3148,8 @@ The following type codes are defined:\n\ 'Q' unsigned integer 8 (see note)\n\ 'f' floating-point 4\n\ 'd' floating-point 8\n\ + 'F' float complex 8\n\ + 'D' double complex 16\n\ \n\ NOTE: The 'u' typecode corresponds to Python's unicode character. On\n\ narrow builds this is 2-bytes on wide builds this is 4-bytes.\n\ From 5d52ad60ed2c5adbd10d1d5db1f211b1f60013c3 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Sat, 21 Mar 2026 06:54:52 +0300 Subject: [PATCH 2/5] +make clinic --- Modules/arraymodule.c | 2 +- Modules/clinic/arraymodule.c.h | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Modules/arraymodule.c b/Modules/arraymodule.c index 661af86f483363..b0c4eb4e3b38c2 100644 --- a/Modules/arraymodule.c +++ b/Modules/arraymodule.c @@ -1568,7 +1568,7 @@ components (the real part, followed by imaginary part) is preserved. static PyObject * array_array_byteswap_impl(arrayobject *self) -/*[clinic end generated code: output=5f8236cbdf0d90b5 input=9af1d1749000b14f]*/ +/*[clinic end generated code: output=5f8236cbdf0d90b5 input=c1cff4f9b5badd95]*/ { char *p; Py_ssize_t i; diff --git a/Modules/clinic/arraymodule.c.h b/Modules/clinic/arraymodule.c.h index 2648583c654a04..cab3431b044601 100644 --- a/Modules/clinic/arraymodule.c.h +++ b/Modules/clinic/arraymodule.c.h @@ -335,8 +335,9 @@ PyDoc_STRVAR(array_array_byteswap__doc__, "\n" "Byteswap all items of the array.\n" "\n" -"If the items in the array are not 1, 2, 4, or 8 bytes in size, RuntimeError is\n" -"raised."); +"If the items in the array are not 1, 2, 4, 8 or 16 bytes in size, RuntimeError is\n" +"raised. Note, that for complex types the order of\n" +"components (the real part, followed by imaginary part) is preserved."); #define ARRAY_ARRAY_BYTESWAP_METHODDEF \ {"byteswap", (PyCFunction)array_array_byteswap, METH_NOARGS, array_array_byteswap__doc__}, @@ -778,4 +779,4 @@ array_arrayiterator___setstate__(PyObject *self, PyObject *state) return return_value; } -/*[clinic end generated code: output=c993c3598085840e input=a9049054013a1b77]*/ +/*[clinic end generated code: output=624d909ed52bc2c4 input=a9049054013a1b77]*/ From f5dfb79b356dae2908405f06f6a3b60fb265a863 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Sat, 21 Mar 2026 10:20:56 +0300 Subject: [PATCH 3/5] + cleanup news entry --- Doc/whatsnew/3.15.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 2199dc2e4118b3..dfcc7a67256eb7 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -632,9 +632,8 @@ argparse array ----- -* Support the :c:expr:`float complex` and :c:expr:`double complex` C types in - the :mod:`array` module (formatting characters ``'F'`` and ``'D'`` - respectively). +* Support the :c:expr:`float complex` and :c:expr:`double complex` C types + (formatting characters ``'F'`` and ``'D'`` respectively). (Contributed by Sergey B Kirpichev in :gh:`146151`.) From a592361647b3ac6aed8853eae14a9304462805bb Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Sat, 21 Mar 2026 10:19:42 +0300 Subject: [PATCH 4/5] gh-146238: support half-floats in the array module --- Doc/library/array.rst | 17 +++++- Doc/whatsnew/3.15.rst | 7 ++- Lib/test/test_array.py | 11 +++- ...-03-21-10-02-20.gh-issue-146238.2WpMOj.rst | 2 + Modules/arraymodule.c | 60 ++++++++++++++++++- 5 files changed, 88 insertions(+), 9 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-03-21-10-02-20.gh-issue-146238.2WpMOj.rst diff --git a/Doc/library/array.rst b/Doc/library/array.rst index dadae6ff71a56a..0eea922fd388a3 100644 --- a/Doc/library/array.rst +++ b/Doc/library/array.rst @@ -42,13 +42,15 @@ defined: +-----------+--------------------+-------------------+-----------------------+-------+ | ``'Q'`` | unsigned long long | int | 8 | | +-----------+--------------------+-------------------+-----------------------+-------+ +| ``'e'`` | _Float16 | float | 2 | \(3) | ++-----------+--------------------+-------------------+-----------------------+-------+ | ``'f'`` | float | float | 4 | | +-----------+--------------------+-------------------+-----------------------+-------+ | ``'d'`` | double | float | 8 | | +-----------+--------------------+-------------------+-----------------------+-------+ -| ``'F'`` | float complex | complex | 8 | \(3) | +| ``'F'`` | float complex | complex | 8 | \(4) | +-----------+--------------------+-------------------+-----------------------+-------+ -| ``'D'`` | double complex | complex | 16 | \(3) | +| ``'D'`` | double complex | complex | 16 | \(4) | +-----------+--------------------+-------------------+-----------------------+-------+ @@ -69,6 +71,15 @@ Notes: .. versionadded:: 3.13 (3) + The IEEE 754 binary16 "half precision" type was introduced in the 2008 + revision of the `IEEE 754 standard `_. + This type is not widely supported by C compilers. It's available + as :c:expr:`_Float16` type, if the compiler supports the Annex H + of the C23 standard. + + .. versionadded:: 3.15 + +(4) Complex types (``F`` and ``D``) are available unconditionally, regardless on support for complex types (the Annex G of the C11 standard) by the C compiler. @@ -297,3 +308,5 @@ Examples:: `NumPy `_ The NumPy package defines another array type. + +.. _ieee 754 standard: https://en.wikipedia.org/wiki/IEEE_754-2008_revision diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index dfcc7a67256eb7..acce4306141d24 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -632,9 +632,10 @@ argparse array ----- -* Support the :c:expr:`float complex` and :c:expr:`double complex` C types - (formatting characters ``'F'`` and ``'D'`` respectively). - (Contributed by Sergey B Kirpichev in :gh:`146151`.) +* Support half-floats (16-bit IEEE 754 binary interchange format, formatting + character ``'e'``), the :c:expr:`float complex` and :c:expr:`double complex` + C types (formatting characters ``'F'`` and ``'D'`` respectively). + (Contributed by Sergey B Kirpichev in :gh:`146151` and :gh:`146238`.) base64 diff --git a/Lib/test/test_array.py b/Lib/test/test_array.py index 24148917f103fe..591b37ff59b8a3 100755 --- a/Lib/test/test_array.py +++ b/Lib/test/test_array.py @@ -31,7 +31,7 @@ class ArraySubclassWithKwargs(array.array): def __init__(self, typecode, newarg=None): array.array.__init__(self) -typecodes = 'uwbBhHiIlLfdqQFD' +typecodes = 'uwbBhHiIlLfdqQFDe' class MiscTest(unittest.TestCase): @@ -143,7 +143,7 @@ def test_error(self): self.assertRaises(ValueError, array_reconstructor, array.array, "b", UNKNOWN_FORMAT, b"") self.assertRaises(ValueError, array_reconstructor, - array.array, "b", 26, b"") + array.array, "b", 28, b"") self.assertRaises(ValueError, array_reconstructor, array.array, "d", 16, b"a") @@ -1578,6 +1578,13 @@ def test_byteswap(self): self.assertEqual(a, b) +class HalfFloatTest(FPTest, unittest.TestCase): + example = [-42.0, 0, 42, 1e3, -1e3] + smallerexample = [-42.0, 0, 42, 1e3, -2e3] + biggerexample = [-42.0, 0, 42, 1e3, 1e3] + typecode = 'e' + minitemsize = 2 + class FloatTest(FPTest, unittest.TestCase): typecode = 'f' minitemsize = 4 diff --git a/Misc/NEWS.d/next/Library/2026-03-21-10-02-20.gh-issue-146238.2WpMOj.rst b/Misc/NEWS.d/next/Library/2026-03-21-10-02-20.gh-issue-146238.2WpMOj.rst new file mode 100644 index 00000000000000..35e951e38e4152 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-03-21-10-02-20.gh-issue-146238.2WpMOj.rst @@ -0,0 +1,2 @@ +Support half-floats (type code ``'e'`` of the :mod:`struct` module) in the +:mod:`array` module. Patch by Sergey B Kirpichev. diff --git a/Modules/arraymodule.c b/Modules/arraymodule.c index b0c4eb4e3b38c2..a7d485f35f39c6 100644 --- a/Modules/arraymodule.c +++ b/Modules/arraymodule.c @@ -122,10 +122,12 @@ enum machine_format_code { IEEE_754_FLOAT_COMPLEX_LE = 22, IEEE_754_FLOAT_COMPLEX_BE = 23, IEEE_754_DOUBLE_COMPLEX_LE = 24, - IEEE_754_DOUBLE_COMPLEX_BE = 25 + IEEE_754_DOUBLE_COMPLEX_BE = 25, + IEEE_754_FLOAT16_LE = 26, + IEEE_754_FLOAT16_BE = 27 }; #define MACHINE_FORMAT_CODE_MIN 0 -#define MACHINE_FORMAT_CODE_MAX 25 +#define MACHINE_FORMAT_CODE_MAX 27 /* @@ -614,6 +616,32 @@ QQ_setitem(arrayobject *ap, Py_ssize_t i, PyObject *v) return 0; } +static PyObject * +e_getitem(arrayobject *ap, Py_ssize_t i) +{ + double x = PyFloat_Unpack2(ap->ob_item + sizeof(short)*i, + PY_LITTLE_ENDIAN); + + return PyFloat_FromDouble(x); +} + +static int +e_setitem(arrayobject *ap, Py_ssize_t i, PyObject *v) +{ + float x; + if (!PyArg_Parse(v, "f;array item must be float", &x)) { + return -1; + } + + CHECK_ARRAY_BOUNDS(ap, i); + + if (i >= 0) { + return PyFloat_Pack2(x, ap->ob_item + sizeof(short)*i, + PY_LITTLE_ENDIAN); + } + return 0; +} + static PyObject * f_getitem(arrayobject *ap, Py_ssize_t i) { @@ -754,6 +782,7 @@ static const struct arraydescr descriptors[] = { {'L', sizeof(long), LL_getitem, LL_setitem, LL_compareitems, "L", 1, 0}, {'q', sizeof(long long), q_getitem, q_setitem, q_compareitems, "q", 1, 1}, {'Q', sizeof(long long), QQ_getitem, QQ_setitem, QQ_compareitems, "Q", 1, 0}, + {'e', sizeof(short), e_getitem, e_setitem, NULL, "e", 0, 0}, {'f', sizeof(float), f_getitem, f_setitem, NULL, "f", 0, 0}, {'d', sizeof(double), d_getitem, d_setitem, NULL, "d", 0, 0}, {'F', 2*sizeof(float), cf_getitem, cf_setitem, NULL, "F", 0, 0}, @@ -2093,6 +2122,8 @@ static const struct mformatdescr { {8, 0, 1}, /* 23: IEEE_754_FLOAT_COMPLEX_BE */ {16, 0, 0}, /* 24: IEEE_754_DOUBLE_COMPLEX_LE */ {16, 0, 1}, /* 25: IEEE_754_DOUBLE_COMPLEX_BE */ + {2, 0, 0}, /* 26: IEEE_754_FLOAT16_LE */ + {2, 0, 1} /* 27: IEEE_754_FLOAT16_BE */ }; @@ -2127,6 +2158,9 @@ typecode_to_mformat_code(char typecode) case 'w': return UTF32_LE + is_big_endian; + case 'e': + return _PY_FLOAT_BIG_ENDIAN ? IEEE_754_FLOAT16_BE : IEEE_754_FLOAT16_LE; + case 'f': if (sizeof(float) == 4) { const float y = 16711938.0; @@ -2326,6 +2360,27 @@ array__array_reconstructor_impl(PyObject *module, PyTypeObject *arraytype, return NULL; } switch (mformat_code) { + case IEEE_754_FLOAT16_LE: + case IEEE_754_FLOAT16_BE: { + Py_ssize_t i; + int le = (mformat_code == IEEE_754_FLOAT_LE) ? 1 : 0; + Py_ssize_t itemcount = Py_SIZE(items) / 2; + const char *memstr = PyBytes_AS_STRING(items); + + converted_items = PyList_New(itemcount); + if (converted_items == NULL) + return NULL; + for (i = 0; i < itemcount; i++) { + PyObject *pyfloat = PyFloat_FromDouble( + PyFloat_Unpack2(&memstr[i * 2], le)); + if (pyfloat == NULL) { + Py_DECREF(converted_items); + return NULL; + } + PyList_SET_ITEM(converted_items, i, pyfloat); + } + break; + } case IEEE_754_FLOAT_LE: case IEEE_754_FLOAT_BE: { Py_ssize_t i; @@ -3146,6 +3201,7 @@ The following type codes are defined:\n\ 'L' unsigned integer 4\n\ 'q' signed integer 8 (see note)\n\ 'Q' unsigned integer 8 (see note)\n\ + 'e' 16-bit IEEE floats 2\n\ 'f' floating-point 4\n\ 'd' floating-point 8\n\ 'F' float complex 8\n\ From b4f641ef5d22c4b2348279c1ca3dd136b9713f11 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Sat, 21 Mar 2026 10:30:56 +0300 Subject: [PATCH 5/5] add _Float16 to nitpick_ignore --- Doc/conf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Doc/conf.py b/Doc/conf.py index 4ac6f6192a0806..07e0d113a24c10 100644 --- a/Doc/conf.py +++ b/Doc/conf.py @@ -177,6 +177,7 @@ ('c:type', '__int64'), ('c:type', 'unsigned __int64'), ('c:type', 'double'), + ('c:type', '_Float16'), # Standard C structures ('c:struct', 'in6_addr'), ('c:struct', 'in_addr'),