From 6b37e6648c70d5d66fdf7260f3d138a99a88a02f Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Fri, 24 Oct 2025 13:27:11 +0100 Subject: [PATCH 1/9] Force alignment of empty `bytearray` and `array.array` buffers This ensures the buffers used by the empty `bytearray` and `array.array` are aligned the same as a pointer returned by the allocator. This is a more convenient default for interop with other languages that have stricter requirements of type-safe buffers (e.g. Rust's `&[T]` type) even when empty. --- Misc/ACKS | 1 + .../2025-10-24-17-30-51.gh-issue-140557.X2GETk.rst | 3 +++ Modules/arraymodule.c | 2 +- Objects/bytearrayobject.c | 2 +- 4 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-10-24-17-30-51.gh-issue-140557.X2GETk.rst diff --git a/Misc/ACKS b/Misc/ACKS index f5f15f2eb7ea24..5bfbc38587b58e 100644 --- a/Misc/ACKS +++ b/Misc/ACKS @@ -1148,6 +1148,7 @@ Per Lindqvist Eric Lindvall Gregor Lingl Everett Lipman +Jake Lishman Mirko Liss Alexander Liu Hui Liu diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-10-24-17-30-51.gh-issue-140557.X2GETk.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-10-24-17-30-51.gh-issue-140557.X2GETk.rst new file mode 100644 index 00000000000000..e1ba0146db5787 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-10-24-17-30-51.gh-issue-140557.X2GETk.rst @@ -0,0 +1,3 @@ +The empty instances of :func:`bytearray` and :func:`array.array` now produce +buffers that are aligned for any data type. This is a convenience for +interoperation with other languages. diff --git a/Modules/arraymodule.c b/Modules/arraymodule.c index 729e085c19f006..1129314067e15e 100644 --- a/Modules/arraymodule.c +++ b/Modules/arraymodule.c @@ -2655,7 +2655,7 @@ array_ass_subscr(PyObject *op, PyObject *item, PyObject *value) } } -static const void *emptybuf = ""; +static const _Py_ALIGNED_DEF(ALIGNOF_MAX_ALIGN_T, char) emptybuf[] = ""; static int diff --git a/Objects/bytearrayobject.c b/Objects/bytearrayobject.c index a73bfff340ce48..af01393a763ac2 100644 --- a/Objects/bytearrayobject.c +++ b/Objects/bytearrayobject.c @@ -18,7 +18,7 @@ class bytearray "PyByteArrayObject *" "&PyByteArray_Type" /*[clinic end generated code: output=da39a3ee5e6b4b0d input=5535b77c37a119e0]*/ /* For PyByteArray_AS_STRING(). */ -char _PyByteArray_empty_string[] = ""; +_Py_ALIGNED_DEF(ALIGNOF_MAX_ALIGN_T, char) _PyByteArray_empty_string[] = ""; /* Helpers */ From 1fea8e5ca08e5e62a4e10c799f73c1529b0b9667 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Thu, 30 Oct 2025 00:28:53 +0000 Subject: [PATCH 2/9] Add tests of buffer pointer alignment --- Lib/test/support/__init__.py | 36 ++++++++++++++++++++++++++++++++++++ Lib/test/test_array.py | 16 ++++++++++++++++ Lib/test/test_builtin.py | 9 +++++++++ 3 files changed, 61 insertions(+) diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py index 098bdcc0542b90..d0f34582dee32b 100644 --- a/Lib/test/support/__init__.py +++ b/Lib/test/support/__init__.py @@ -5,6 +5,7 @@ import annotationlib import contextlib +import ctypes import functools import inspect import logging @@ -69,6 +70,7 @@ "in_systemd_nspawn_sync_suppressed", "run_no_yield_async_fn", "run_yielding_async_fn", "async_yield", "reset_code", + "ctypes_py_buffer", ] @@ -3183,3 +3185,37 @@ def linked_to_musl(): return _linked_to_musl _linked_to_musl = tuple(map(int, version.split('.'))) return _linked_to_musl + + +class _py_buffer(ctypes.Structure): + _fields_ = [ + ("buf", ctypes.c_void_p), + ("obj", ctypes.py_object), + ("len", ctypes.c_ssize_t), + ("itemsize", ctypes.c_ssize_t), + ("readonly", ctypes.c_int), + ("ndim", ctypes.c_int), + ("format", ctypes.c_char_p), + ("shape", ctypes.POINTER(ctypes.c_ssize_t)), + ("strides", ctypes.POINTER(ctypes.c_ssize_t)), + ("suboffsets", ctypes.POINTER(ctypes.c_ssize_t)), + ("internal", ctypes.c_void_p), + ] + + +@contextlib.contextmanager +def ctypes_py_buffer(ob, flags=inspect.BufferFlags.SIMPLE): + """ + Safely acquire a `Py_buffer` as a ctypes struct. + + `ob` must implement the buffer protocol, and the retrieved buffer is + released on exit of the context manager. + """ + buf = _py_buffer() + ctypes.pythonapi.PyObject_GetBuffer(ctypes.py_object(ob), + ctypes.byref(buf), + ctypes.c_int(flags)) + try: + yield buf + finally: + ctypes.pythonapi.PyBuffer_Release(ctypes.byref(buf)) diff --git a/Lib/test/test_array.py b/Lib/test/test_array.py index 83b3c978da3581..41560696e089e8 100755 --- a/Lib/test/test_array.py +++ b/Lib/test/test_array.py @@ -8,6 +8,8 @@ from test.support import import_helper from test.support import os_helper from test.support import _2G +from test.support import ctypes_py_buffer +import ctypes import weakref import pickle import operator @@ -67,6 +69,20 @@ def test_empty(self): a += a self.assertEqual(len(a), 0) + def test_empty_alignment(self): + # gh-140557: pointer alignment of empty allocation + self.enterContext(warnings.catch_warnings()) + warnings.filterwarnings( + "ignore", + message="The 'u' type code is deprecated and " + "will be removed in Python 3.16", + category=DeprecationWarning) + max_align = ctypes.alignment(ctypes.c_longdouble) + for typecode in typecodes: + a = array.array(typecode) + with ctypes_py_buffer(a) as buf: + self.assertEqual(buf.buf % max_align, 0) + # Machine format codes. # diff --git a/Lib/test/test_builtin.py b/Lib/test/test_builtin.py index fe3e391a7f5ba1..070d7b8f1354fe 100644 --- a/Lib/test/test_builtin.py +++ b/Lib/test/test_builtin.py @@ -4,6 +4,7 @@ import builtins import collections import contextlib +import ctypes import decimal import fractions import gc @@ -38,6 +39,7 @@ from test.support.testcase import ComplexesAreIdenticalMixin from test.support.warnings_helper import check_warnings from test.support import requires_IEEE_754 +from test.support import ctypes_py_buffer from unittest.mock import MagicMock, patch try: import pty, signal @@ -2404,6 +2406,13 @@ def iterator(): yield b'B' self.assertEqual(bytearray(b'A,B'), array.join(iterator())) + def test_bytearray_empty_alignment(self): + # gh-140557: alignment of pointer in empty allocation + max_align = ctypes.alignment(ctypes.c_longdouble) + array = bytearray() + with ctypes_py_buffer(array) as buf: + self.assertEqual(buf.buf % max_align, 0) + def test_construct_singletons(self): for const in None, Ellipsis, NotImplemented: tp = type(const) From b7eed2bb88c0c569a62d771b151ae8a03554c72c Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Thu, 30 Oct 2025 00:35:39 +0000 Subject: [PATCH 3/9] Make NEWS more concise --- .../2025-10-24-17-30-51.gh-issue-140557.X2GETk.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-10-24-17-30-51.gh-issue-140557.X2GETk.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-10-24-17-30-51.gh-issue-140557.X2GETk.rst index e1ba0146db5787..cceebc7ae49513 100644 --- a/Misc/NEWS.d/next/Core_and_Builtins/2025-10-24-17-30-51.gh-issue-140557.X2GETk.rst +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-10-24-17-30-51.gh-issue-140557.X2GETk.rst @@ -1,3 +1,2 @@ -The empty instances of :func:`bytearray` and :func:`array.array` now produce -buffers that are aligned for any data type. This is a convenience for -interoperation with other languages. +:func:`bytearray` and :func:`array.array` now default to aligned buffers when +empty. Unaligned buffers can still be created by slicing. From 9d454de46dcae33514c208c6e2eab799e9dd62b0 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Thu, 30 Oct 2025 00:58:02 +0000 Subject: [PATCH 4/9] Avoid `ctypes` import on unsupported platforms --- Lib/test/support/__init__.py | 39 ++++++++++++++++++++++-------------- Lib/test/test_array.py | 2 +- Lib/test/test_builtin.py | 2 +- 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py index 517786b89e97e8..4d7a3cbff5567d 100644 --- a/Lib/test/support/__init__.py +++ b/Lib/test/support/__init__.py @@ -5,7 +5,6 @@ import annotationlib import contextlib -import ctypes import functools import inspect import logging @@ -3187,20 +3186,25 @@ def linked_to_musl(): return _linked_to_musl -class _py_buffer(ctypes.Structure): - _fields_ = [ - ("buf", ctypes.c_void_p), - ("obj", ctypes.py_object), - ("len", ctypes.c_ssize_t), - ("itemsize", ctypes.c_ssize_t), - ("readonly", ctypes.c_int), - ("ndim", ctypes.c_int), - ("format", ctypes.c_char_p), - ("shape", ctypes.POINTER(ctypes.c_ssize_t)), - ("strides", ctypes.POINTER(ctypes.c_ssize_t)), - ("suboffsets", ctypes.POINTER(ctypes.c_ssize_t)), - ("internal", ctypes.c_void_p), - ] +try: + import ctypes + + class _py_buffer(ctypes.Structure): + _fields_ = [ + ("buf", ctypes.c_void_p), + ("obj", ctypes.py_object), + ("len", ctypes.c_ssize_t), + ("itemsize", ctypes.c_ssize_t), + ("readonly", ctypes.c_int), + ("ndim", ctypes.c_int), + ("format", ctypes.c_char_p), + ("shape", ctypes.POINTER(ctypes.c_ssize_t)), + ("strides", ctypes.POINTER(ctypes.c_ssize_t)), + ("suboffsets", ctypes.POINTER(ctypes.c_ssize_t)), + ("internal", ctypes.c_void_p), + ] +except ImportError: + _py_buffer = None @contextlib.contextmanager @@ -3210,7 +3214,12 @@ def ctypes_py_buffer(ob, flags=inspect.BufferFlags.SIMPLE): `ob` must implement the buffer protocol, and the retrieved buffer is released on exit of the context manager. + + Skips any test using it if `ctypes` is unavailable. """ + from .import_helper import import_module + + ctypes = import_module("ctypes") buf = _py_buffer() ctypes.pythonapi.PyObject_GetBuffer(ctypes.py_object(ob), ctypes.byref(buf), diff --git a/Lib/test/test_array.py b/Lib/test/test_array.py index 41560696e089e8..420186fb8bf1b3 100755 --- a/Lib/test/test_array.py +++ b/Lib/test/test_array.py @@ -9,7 +9,6 @@ from test.support import os_helper from test.support import _2G from test.support import ctypes_py_buffer -import ctypes import weakref import pickle import operator @@ -71,6 +70,7 @@ def test_empty(self): def test_empty_alignment(self): # gh-140557: pointer alignment of empty allocation + ctypes = import_helper.import_module("ctypes") self.enterContext(warnings.catch_warnings()) warnings.filterwarnings( "ignore", diff --git a/Lib/test/test_builtin.py b/Lib/test/test_builtin.py index 070d7b8f1354fe..e585a5852175bd 100644 --- a/Lib/test/test_builtin.py +++ b/Lib/test/test_builtin.py @@ -4,7 +4,6 @@ import builtins import collections import contextlib -import ctypes import decimal import fractions import gc @@ -2408,6 +2407,7 @@ def iterator(): def test_bytearray_empty_alignment(self): # gh-140557: alignment of pointer in empty allocation + ctypes = import_module("ctypes") max_align = ctypes.alignment(ctypes.c_longdouble) array = bytearray() with ctypes_py_buffer(array) as buf: From ad966dcf2c921d87c9d27809c75ca6dfbd7afff5 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Tue, 6 Jan 2026 16:30:46 +0000 Subject: [PATCH 5/9] Revert Python-space `ctypes` tests This reverts commit 9d454de46dcae33514c208c6e2eab799e9dd62b0. This reverts commit 1fea8e5ca08e5e62a4e10c799f73c1529b0b9667. --- Lib/test/support/__init__.py | 46 +----------------------------------- Lib/test/test_array.py | 16 ------------- Lib/test/test_builtin.py | 9 ------- 3 files changed, 1 insertion(+), 70 deletions(-) diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py index c0e4853ee3b197..84fd43fd396914 100644 --- a/Lib/test/support/__init__.py +++ b/Lib/test/support/__init__.py @@ -69,7 +69,7 @@ "BrokenIter", "in_systemd_nspawn_sync_suppressed", "run_no_yield_async_fn", "run_yielding_async_fn", "async_yield", - "reset_code", "on_github_actions", "ctypes_py_buffer", + "reset_code", "on_github_actions" ] @@ -3272,47 +3272,3 @@ def linked_to_musl(): return _linked_to_musl _linked_to_musl = tuple(map(int, version.split('.'))) return _linked_to_musl - - -try: - import ctypes - - class _py_buffer(ctypes.Structure): - _fields_ = [ - ("buf", ctypes.c_void_p), - ("obj", ctypes.py_object), - ("len", ctypes.c_ssize_t), - ("itemsize", ctypes.c_ssize_t), - ("readonly", ctypes.c_int), - ("ndim", ctypes.c_int), - ("format", ctypes.c_char_p), - ("shape", ctypes.POINTER(ctypes.c_ssize_t)), - ("strides", ctypes.POINTER(ctypes.c_ssize_t)), - ("suboffsets", ctypes.POINTER(ctypes.c_ssize_t)), - ("internal", ctypes.c_void_p), - ] -except ImportError: - _py_buffer = None - - -@contextlib.contextmanager -def ctypes_py_buffer(ob, flags=inspect.BufferFlags.SIMPLE): - """ - Safely acquire a `Py_buffer` as a ctypes struct. - - `ob` must implement the buffer protocol, and the retrieved buffer is - released on exit of the context manager. - - Skips any test using it if `ctypes` is unavailable. - """ - from .import_helper import import_module - - ctypes = import_module("ctypes") - buf = _py_buffer() - ctypes.pythonapi.PyObject_GetBuffer(ctypes.py_object(ob), - ctypes.byref(buf), - ctypes.c_int(flags)) - try: - yield buf - finally: - ctypes.pythonapi.PyBuffer_Release(ctypes.byref(buf)) diff --git a/Lib/test/test_array.py b/Lib/test/test_array.py index 420186fb8bf1b3..83b3c978da3581 100755 --- a/Lib/test/test_array.py +++ b/Lib/test/test_array.py @@ -8,7 +8,6 @@ from test.support import import_helper from test.support import os_helper from test.support import _2G -from test.support import ctypes_py_buffer import weakref import pickle import operator @@ -68,21 +67,6 @@ def test_empty(self): a += a self.assertEqual(len(a), 0) - def test_empty_alignment(self): - # gh-140557: pointer alignment of empty allocation - ctypes = import_helper.import_module("ctypes") - self.enterContext(warnings.catch_warnings()) - warnings.filterwarnings( - "ignore", - message="The 'u' type code is deprecated and " - "will be removed in Python 3.16", - category=DeprecationWarning) - max_align = ctypes.alignment(ctypes.c_longdouble) - for typecode in typecodes: - a = array.array(typecode) - with ctypes_py_buffer(a) as buf: - self.assertEqual(buf.buf % max_align, 0) - # Machine format codes. # diff --git a/Lib/test/test_builtin.py b/Lib/test/test_builtin.py index 8c43ce5bc8d359..ce60a5d095dd52 100644 --- a/Lib/test/test_builtin.py +++ b/Lib/test/test_builtin.py @@ -38,7 +38,6 @@ from test.support.testcase import ComplexesAreIdenticalMixin from test.support.warnings_helper import check_warnings from test.support import requires_IEEE_754 -from test.support import ctypes_py_buffer from unittest.mock import MagicMock, patch try: import pty, signal @@ -2428,14 +2427,6 @@ def iterator(): yield b'B' self.assertEqual(bytearray(b'A,B'), array.join(iterator())) - def test_bytearray_empty_alignment(self): - # gh-140557: alignment of pointer in empty allocation - ctypes = import_module("ctypes") - max_align = ctypes.alignment(ctypes.c_longdouble) - array = bytearray() - with ctypes_py_buffer(array) as buf: - self.assertEqual(buf.buf % max_align, 0) - def test_construct_singletons(self): for const in None, Ellipsis, NotImplemented: tp = type(const) From b03621506b270549f3227e797bee4926c4165c9b Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Tue, 6 Jan 2026 17:26:39 +0000 Subject: [PATCH 6/9] Add alignment tests with `_testcapi` helper --- Lib/test/test_buffer.py | 37 +++++++++++++++++++++++++++++++++++++ Modules/_testcapi/buffer.c | 24 ++++++++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/Lib/test/test_buffer.py b/Lib/test/test_buffer.py index 19582e757161fc..d2b5ebb8f25b5b 100644 --- a/Lib/test/test_buffer.py +++ b/Lib/test/test_buffer.py @@ -4447,6 +4447,43 @@ def test_bytearray_release_buffer_read_flag(self): with self.assertRaises(SystemError): obj.__buffer__(inspect.BufferFlags.WRITE) + @support.cpython_only + @unittest.skipIf(_testcapi is None, "requires _testcapi") + @unittest.skipIf(ctypes is None, "requires ctypes") + def test_bytearray_alignment(self): + # gh-140557: pointer alignment of buffers including empty allocation + max_align = ctypes.alignment(ctypes.c_longdouble) + cases = [ + bytearray(), + bytearray(1), + bytearray(b"0123456789abcdef"), + bytearray(16), + ] + ptrs = [_testcapi.buffer_pointer_as_int(array) for array in cases] + self.assertEqual([ptr % max_align for ptr in ptrs], [0]*len(ptrs)) + + @support.cpython_only + @unittest.skipIf(_testcapi is None, "requires _testcapi") + @unittest.skipIf(ctypes is None, "requires ctypes") + def test_array_alignment(self): + # gh-140557: pointer alignment of buffers including empty allocation + max_align = ctypes.alignment(ctypes.c_longdouble) + cases = [array.array(fmt) for fmt in ARRAY] + # Empty arrays + self.assertEqual( + [_testcapi.buffer_pointer_as_int(case) % max_align + for case in cases], + [0] * len(cases), + ) + for case in cases: + case.append(0) + # Allocated arrays + self.assertEqual( + [_testcapi.buffer_pointer_as_int(case) % max_align + for case in cases], + [0] * len(cases), + ) + @support.cpython_only def test_pybuffer_size_from_format(self): # basic tests diff --git a/Modules/_testcapi/buffer.c b/Modules/_testcapi/buffer.c index e63d4179824529..32c80e7685a2eb 100644 --- a/Modules/_testcapi/buffer.c +++ b/Modules/_testcapi/buffer.c @@ -98,6 +98,27 @@ static PyTypeObject testBufType = { .tp_members = testbuf_members }; +/* Get the pointer from a buffer-supporting object as a PyLong. + * + * Used to test alignment properties.*/ +static PyObject * +buffer_pointer_as_int(PyObject *Py_UNUSED(module), PyObject *obj) +{ + PyObject *out; + Py_buffer view; + if (PyObject_GetBuffer(obj, &view, PyBUF_SIMPLE) != 0) { + return NULL; + } + out = PyLong_FromVoidPtr(view.buf); + PyBuffer_Release(&view); + return out; +} + +static PyMethodDef test_methods[] = { + {"buffer_pointer_as_int", buffer_pointer_as_int, METH_O}, + {NULL}, +}; + int _PyTestCapi_Init_Buffer(PyObject *m) { if (PyType_Ready(&testBufType) < 0) { @@ -106,6 +127,9 @@ _PyTestCapi_Init_Buffer(PyObject *m) { if (PyModule_AddObjectRef(m, "testBuf", (PyObject *)&testBufType)) { return -1; } + if (PyModule_AddFunctions(m, test_methods) < 0) { + return -1; + } return 0; } From 2dec49067e21e19df3a7ba885d6951e040177922 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Tue, 6 Jan 2026 18:22:30 +0000 Subject: [PATCH 7/9] Slacken test to match 'size_t' only --- Lib/test/test_buffer.py | 18 +++++++++--------- ...5-10-24-17-30-51.gh-issue-140557.X2GETk.rst | 4 ++-- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Lib/test/test_buffer.py b/Lib/test/test_buffer.py index d2b5ebb8f25b5b..726c2a909e2109 100644 --- a/Lib/test/test_buffer.py +++ b/Lib/test/test_buffer.py @@ -4449,10 +4449,11 @@ def test_bytearray_release_buffer_read_flag(self): @support.cpython_only @unittest.skipIf(_testcapi is None, "requires _testcapi") - @unittest.skipIf(ctypes is None, "requires ctypes") + @unittest.skipIf(struct is None, "requires struct") def test_bytearray_alignment(self): # gh-140557: pointer alignment of buffers including empty allocation - max_align = ctypes.alignment(ctypes.c_longdouble) + # should be at least to `size_t`. + align = struct.calcsize("N") cases = [ bytearray(), bytearray(1), @@ -4460,27 +4461,26 @@ def test_bytearray_alignment(self): bytearray(16), ] ptrs = [_testcapi.buffer_pointer_as_int(array) for array in cases] - self.assertEqual([ptr % max_align for ptr in ptrs], [0]*len(ptrs)) + self.assertEqual([ptr % align for ptr in ptrs], [0]*len(ptrs)) @support.cpython_only @unittest.skipIf(_testcapi is None, "requires _testcapi") - @unittest.skipIf(ctypes is None, "requires ctypes") + @unittest.skipIf(struct is None, "requires struct") def test_array_alignment(self): # gh-140557: pointer alignment of buffers including empty allocation - max_align = ctypes.alignment(ctypes.c_longdouble) + # should be at least to `size_t`. + align = struct.calcsize("N") cases = [array.array(fmt) for fmt in ARRAY] # Empty arrays self.assertEqual( - [_testcapi.buffer_pointer_as_int(case) % max_align - for case in cases], + [_testcapi.buffer_pointer_as_int(case) % align for case in cases], [0] * len(cases), ) for case in cases: case.append(0) # Allocated arrays self.assertEqual( - [_testcapi.buffer_pointer_as_int(case) % max_align - for case in cases], + [_testcapi.buffer_pointer_as_int(case) % align for case in cases], [0] * len(cases), ) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-10-24-17-30-51.gh-issue-140557.X2GETk.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-10-24-17-30-51.gh-issue-140557.X2GETk.rst index cceebc7ae49513..21170a954b659a 100644 --- a/Misc/NEWS.d/next/Core_and_Builtins/2025-10-24-17-30-51.gh-issue-140557.X2GETk.rst +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-10-24-17-30-51.gh-issue-140557.X2GETk.rst @@ -1,2 +1,2 @@ -:func:`bytearray` and :func:`array.array` now default to aligned buffers when -empty. Unaligned buffers can still be created by slicing. +:func:`bytearray` and :func:`array.array` buffers now have the same alignment +when empty as when allocated. Unaligned buffers can still be created by slicing. From 92a5fff3f759c5941a76511f82531c137bfa304b Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Wed, 7 Jan 2026 12:00:10 +0000 Subject: [PATCH 8/9] Fixup review comments --- Lib/test/test_buffer.py | 2 -- .../2025-10-24-17-30-51.gh-issue-140557.X2GETk.rst | 2 +- .../next/Library/2026-01-07-11-57-59.gh-issue-140557.3P6-nW.rst | 2 ++ Modules/_testcapi/buffer.c | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-01-07-11-57-59.gh-issue-140557.3P6-nW.rst diff --git a/Lib/test/test_buffer.py b/Lib/test/test_buffer.py index 726c2a909e2109..77d841ace9a449 100644 --- a/Lib/test/test_buffer.py +++ b/Lib/test/test_buffer.py @@ -4449,7 +4449,6 @@ def test_bytearray_release_buffer_read_flag(self): @support.cpython_only @unittest.skipIf(_testcapi is None, "requires _testcapi") - @unittest.skipIf(struct is None, "requires struct") def test_bytearray_alignment(self): # gh-140557: pointer alignment of buffers including empty allocation # should be at least to `size_t`. @@ -4465,7 +4464,6 @@ def test_bytearray_alignment(self): @support.cpython_only @unittest.skipIf(_testcapi is None, "requires _testcapi") - @unittest.skipIf(struct is None, "requires struct") def test_array_alignment(self): # gh-140557: pointer alignment of buffers including empty allocation # should be at least to `size_t`. diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-10-24-17-30-51.gh-issue-140557.X2GETk.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-10-24-17-30-51.gh-issue-140557.X2GETk.rst index 21170a954b659a..d584279a0901b0 100644 --- a/Misc/NEWS.d/next/Core_and_Builtins/2025-10-24-17-30-51.gh-issue-140557.X2GETk.rst +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-10-24-17-30-51.gh-issue-140557.X2GETk.rst @@ -1,2 +1,2 @@ -:func:`bytearray` and :func:`array.array` buffers now have the same alignment +:class:`bytearray` buffers now have the same alignment when empty as when allocated. Unaligned buffers can still be created by slicing. diff --git a/Misc/NEWS.d/next/Library/2026-01-07-11-57-59.gh-issue-140557.3P6-nW.rst b/Misc/NEWS.d/next/Library/2026-01-07-11-57-59.gh-issue-140557.3P6-nW.rst new file mode 100644 index 00000000000000..997ad592bbaafd --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-01-07-11-57-59.gh-issue-140557.3P6-nW.rst @@ -0,0 +1,2 @@ +:class:`array.array` buffers now have the same alignment when empty as when +allocated. Unaligned buffers can still be created by slicing. diff --git a/Modules/_testcapi/buffer.c b/Modules/_testcapi/buffer.c index 32c80e7685a2eb..48393a3dd530c6 100644 --- a/Modules/_testcapi/buffer.c +++ b/Modules/_testcapi/buffer.c @@ -100,7 +100,7 @@ static PyTypeObject testBufType = { /* Get the pointer from a buffer-supporting object as a PyLong. * - * Used to test alignment properties.*/ + * Used to test alignment properties. */ static PyObject * buffer_pointer_as_int(PyObject *Py_UNUSED(module), PyObject *obj) { From d3ca57f9238172ee80e1554fb3305e456b812a7f Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Thu, 8 Jan 2026 10:12:58 +0000 Subject: [PATCH 9/9] Require max observable alignment in array test --- Lib/test/test_buffer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_buffer.py b/Lib/test/test_buffer.py index 77d841ace9a449..ab65a44bda6e7e 100644 --- a/Lib/test/test_buffer.py +++ b/Lib/test/test_buffer.py @@ -4466,8 +4466,8 @@ def test_bytearray_alignment(self): @unittest.skipIf(_testcapi is None, "requires _testcapi") def test_array_alignment(self): # gh-140557: pointer alignment of buffers including empty allocation - # should be at least to `size_t`. - align = struct.calcsize("N") + # should match the maximum array alignment. + align = max(struct.calcsize(fmt) for fmt in ARRAY) cases = [array.array(fmt) for fmt in ARRAY] # Empty arrays self.assertEqual(