From fff147fca41c25cba10655cfab0de0b7f64f6430 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Sat, 10 Jan 2026 16:37:15 +0300 Subject: [PATCH 01/23] gh-78724: deprecate incomplete initialization of struct.Struct() * ``Struct.__new__()`` will require a mandatory argument (format) * Calls of ``__init__()`` method on initialized Struct are deprecated --- Doc/deprecations/pending-removal-in-3.20.rst | 7 +++++++ Doc/whatsnew/3.15.rst | 9 +++++++++ Lib/test/test_struct.py | 10 ++++++++-- .../2026-01-10-16-23-21.gh-issue-78724.HZrfSA.rst | 3 +++ Modules/_struct.c | 13 +++++++++++++ 5 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-01-10-16-23-21.gh-issue-78724.HZrfSA.rst diff --git a/Doc/deprecations/pending-removal-in-3.20.rst b/Doc/deprecations/pending-removal-in-3.20.rst index 4e4b2e1d5f8fff..1907758e8847d8 100644 --- a/Doc/deprecations/pending-removal-in-3.20.rst +++ b/Doc/deprecations/pending-removal-in-3.20.rst @@ -1,6 +1,13 @@ Pending removal in Python 3.20 ------------------------------ +* Calling the ``Struct.__new__()`` without required argument now is + deprecated and will be removed in Python 3.20. Calling + :meth:`~object.__init__` method on initialized :class:`~struct.Struct` + objects is deprecated and will be removed in Python 3.20. + + (Contributed by Sergey B Kirpichev in :gh:`78724`.) + * The ``__version__``, ``version`` and ``VERSION`` attributes have been deprecated in these standard library modules and will be removed in Python 3.20. Use :py:data:`sys.version_info` instead. diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 6cb96c074c78f3..2b6555f70b5c75 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -1137,6 +1137,15 @@ New deprecations (Contributed by Bénédikt Tran in :gh:`134978`.) +* :mod:`struct`: + + * Calling the ``Struct.__new__()`` without required argument now is + deprecated and will be removed in Python 3.20. Calling + :meth:`~object.__init__` method on initialized :class:`~struct.Struct` + objects is deprecated and will be removed in Python 3.20. + + (Contributed by Sergey B Kirpichev in :gh:`78724`.) + * ``__version__`` * The ``__version__``, ``version`` and ``VERSION`` attributes have been diff --git a/Lib/test/test_struct.py b/Lib/test/test_struct.py index bbfe19a4e0bab7..be9960f042208c 100644 --- a/Lib/test/test_struct.py +++ b/Lib/test/test_struct.py @@ -7,6 +7,7 @@ import unittest import struct import sys +import warnings import weakref from test import support @@ -575,7 +576,11 @@ def test_Struct_reinitialization(self): # Struct instance. This test can be used to detect the leak # when running with regrtest -L. s = struct.Struct('i') - s.__init__('ii') + with self.assertWarns(DeprecationWarning): + s.__init__('ii') + with warnings.catch_warnings(): + warnings.simplefilter("error", DeprecationWarning) + self.assertRaises(DeprecationWarning, s.__init__, 'ii') def check_sizeof(self, format_str, number_of_codes): # The size of 'PyStructObject' @@ -783,7 +788,8 @@ class MyStruct(struct.Struct): def __init__(self): super().__init__('>h') - my_struct = MyStruct() + with self.assertWarns(DeprecationWarning): + my_struct = MyStruct() self.assertEqual(my_struct.pack(12345), b'\x30\x39') def test_repr(self): diff --git a/Misc/NEWS.d/next/Library/2026-01-10-16-23-21.gh-issue-78724.HZrfSA.rst b/Misc/NEWS.d/next/Library/2026-01-10-16-23-21.gh-issue-78724.HZrfSA.rst new file mode 100644 index 00000000000000..93c3f6f74f207b --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-01-10-16-23-21.gh-issue-78724.HZrfSA.rst @@ -0,0 +1,3 @@ +Calling the ``Struct.__new__()`` without required argument now is deprecated. +Calling :meth:`~object.__init__` method on initialized :class:`~struct.Struct` +objects is deprecated. Patch by Sergey B Kirpichev. diff --git a/Modules/_struct.c b/Modules/_struct.c index 2acb3df3a30395..5338fe6047b991 100644 --- a/Modules/_struct.c +++ b/Modules/_struct.c @@ -1762,6 +1762,12 @@ s_new(PyTypeObject *type, PyObject *args, PyObject *kwds) { PyObject *self; + if (PyTuple_GET_SIZE(args) != 1 + && PyErr_WarnEx(PyExc_DeprecationWarning, + "Struct().__new__() has one required argument", 1)) + { + return NULL; + } assert(type != NULL); allocfunc alloc_func = PyType_GetSlot(type, Py_tp_alloc); assert(alloc_func != NULL); @@ -1796,6 +1802,13 @@ Struct___init___impl(PyStructObject *self, PyObject *format) { int ret = 0; + if (self->s_codes + && PyErr_WarnEx(PyExc_DeprecationWarning, + ("Explicit call of __init__() on " + "initialized Struct() is deprecated"), 1)) + { + return -1; + } if (PyUnicode_Check(format)) { format = PyUnicode_AsASCIIString(format); if (format == NULL) From 589fbc74b91d352b41c79ed31f1e0dd0fc2b3be4 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Mon, 12 Jan 2026 04:03:21 +0300 Subject: [PATCH 02/23] address review: test_Struct_reinitialization() --- Lib/test/test_struct.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_struct.py b/Lib/test/test_struct.py index b6c299bb9c1515..2e54831b1ff648 100644 --- a/Lib/test/test_struct.py +++ b/Lib/test/test_struct.py @@ -7,7 +7,6 @@ import unittest import struct import sys -import warnings import weakref from test import support @@ -578,9 +577,10 @@ def test_Struct_reinitialization(self): s = struct.Struct('i') with self.assertWarns(DeprecationWarning): s.__init__('ii') - with warnings.catch_warnings(): - warnings.simplefilter("error", DeprecationWarning) - self.assertRaises(DeprecationWarning, s.__init__, 'ii') + self.assertEqual(s.format, 'ii') + packed = b'\x01\x00\x00\x00\x02\x00\x00\x00' + self.assertEqual(s.pack(1, 2), packed) + self.assertEqual(s.unpack(packed), (1, 2)) def check_sizeof(self, format_str, number_of_codes): # The size of 'PyStructObject' From 5e91e878df50a4ee3a14cbf44a5b9db04edf5da5 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Mon, 12 Jan 2026 04:04:48 +0300 Subject: [PATCH 03/23] catch new warning in test_operations_on_half_initialized_Struct() --- Lib/test/test_struct.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_struct.py b/Lib/test/test_struct.py index 2e54831b1ff648..9a31d62d737044 100644 --- a/Lib/test/test_struct.py +++ b/Lib/test/test_struct.py @@ -823,7 +823,8 @@ def test_endian_table_init_subinterpreters(self): self.assertListEqual(list(results), [None] * 5) def test_operations_on_half_initialized_Struct(self): - S = struct.Struct.__new__(struct.Struct) + with self.assertWarns(DeprecationWarning): + S = struct.Struct.__new__(struct.Struct) spam = array.array('b', b' ') self.assertRaises(RuntimeError, S.iter_unpack, spam) From babb274bfe004bdaed46c2437a5e1ab25aa1842a Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Mon, 12 Jan 2026 07:18:11 +0300 Subject: [PATCH 04/23] add init_called flag --- Lib/test/test_struct.py | 2 +- Modules/_struct.c | 90 ++++++++++++++++++++++++----------------- 2 files changed, 53 insertions(+), 39 deletions(-) diff --git a/Lib/test/test_struct.py b/Lib/test/test_struct.py index 9a31d62d737044..b4379ccd9eb7ce 100644 --- a/Lib/test/test_struct.py +++ b/Lib/test/test_struct.py @@ -584,7 +584,7 @@ def test_Struct_reinitialization(self): def check_sizeof(self, format_str, number_of_codes): # The size of 'PyStructObject' - totalsize = support.calcobjsize('2n3P') + totalsize = support.calcobjsize('2n3P1?') # The size taken up by the 'formatcode' dynamic array totalsize += struct.calcsize('P3n0P') * (number_of_codes + 1) support.check_sizeof(self, struct.Struct(format_str), totalsize) diff --git a/Modules/_struct.c b/Modules/_struct.c index 8051f555f39452..d665b0b6e41c3e 100644 --- a/Modules/_struct.c +++ b/Modules/_struct.c @@ -70,6 +70,7 @@ typedef struct { formatcode *s_codes; PyObject *s_format; PyObject *weakreflist; /* List of weak references */ + bool init_called; } PyStructObject; #define PyStructObject_CAST(op) ((PyStructObject *)(op)) @@ -1757,30 +1758,61 @@ prepare_s(PyStructObject *self) return -1; } +static int +actual___init___impl(PyStructObject *self, PyObject *format) +{ + if (PyUnicode_Check(format)) { + format = PyUnicode_AsASCIIString(format); + if (format == NULL) + return -1; + } + else { + Py_INCREF(format); + } + if (!PyBytes_Check(format)) { + Py_DECREF(format); + PyErr_Format(PyExc_TypeError, + "Struct() argument 1 must be a str or bytes object, " + "not %.200s", + _PyType_Name(Py_TYPE(format))); + return -1; + } + Py_SETREF(self->s_format, format); + if (prepare_s(self)) { + return -1; + } + return 0; +} + static PyObject * s_new(PyTypeObject *type, PyObject *args, PyObject *kwds) { - PyObject *self; + PyStructObject *self; - if (PyTuple_GET_SIZE(args) != 1 + if ((PyTuple_GET_SIZE(args) != 1 || kwds) && PyErr_WarnEx(PyExc_DeprecationWarning, - "Struct().__new__() has one required argument", 1)) + "Struct.__new__() has one positional argument", 1)) { return NULL; } assert(type != NULL); allocfunc alloc_func = PyType_GetSlot(type, Py_tp_alloc); assert(alloc_func != NULL); - - self = alloc_func(type, 0); + self = (PyStructObject *)alloc_func(type, 0); if (self != NULL) { - PyStructObject *s = (PyStructObject*)self; - s->s_format = Py_NewRef(Py_None); - s->s_codes = NULL; - s->s_size = -1; - s->s_len = -1; + self->s_format = Py_NewRef(Py_None); + self->s_codes = NULL; + self->s_size = -1; + self->s_len = -1; + self->init_called = false; + if (PyTuple_GET_SIZE(args) > 0) { + if (actual___init___impl(self, PyTuple_GET_ITEM(args, 0))) { + Py_DECREF(self); + return NULL; + } + } } - return self; + return (PyObject *)self; } /*[clinic input] @@ -1800,37 +1832,21 @@ static int Struct___init___impl(PyStructObject *self, PyObject *format) /*[clinic end generated code: output=b8e80862444e92d0 input=192a4575a3dde802]*/ { - int ret = 0; - - if (self->s_codes + if (!self->init_called) { + if (!self->s_codes && actual___init___impl(self, format)) { + return -1; + } + self->init_called = true; + return 0; + } + if ((self->s_codes && self->init_called) && PyErr_WarnEx(PyExc_DeprecationWarning, ("Explicit call of __init__() on " "initialized Struct() is deprecated"), 1)) { return -1; } - if (PyUnicode_Check(format)) { - format = PyUnicode_AsASCIIString(format); - if (format == NULL) - return -1; - } - else { - Py_INCREF(format); - } - - if (!PyBytes_Check(format)) { - Py_DECREF(format); - PyErr_Format(PyExc_TypeError, - "Struct() argument 1 must be a str or bytes object, " - "not %.200s", - _PyType_Name(Py_TYPE(format))); - return -1; - } - - Py_SETREF(self->s_format, format); - - ret = prepare_s(self); - return ret; + return actual___init___impl(self, format); } static int @@ -2473,9 +2489,7 @@ static PyType_Slot PyStructType_slots[] = { {Py_tp_members, s_members}, {Py_tp_getset, s_getsetlist}, {Py_tp_init, Struct___init__}, - {Py_tp_alloc, PyType_GenericAlloc}, {Py_tp_new, s_new}, - {Py_tp_free, PyObject_GC_Del}, {0, 0}, }; From 3f36635ca862290a0a28cc5e7c0066772165cc5d Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Tue, 13 Jan 2026 04:24:42 +0300 Subject: [PATCH 05/23] a hack to support new idiom for subclassing This make format argument in the __init__() - optional. If it's missing, the object must be already initialized in __new__(). --- Modules/_struct.c | 9 +++++++-- Modules/clinic/_struct.c.h | 13 +++++++++---- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/Modules/_struct.c b/Modules/_struct.c index d665b0b6e41c3e..0a72ab53dac520 100644 --- a/Modules/_struct.c +++ b/Modules/_struct.c @@ -1818,7 +1818,7 @@ s_new(PyTypeObject *type, PyObject *args, PyObject *kwds) /*[clinic input] Struct.__init__ - format: object + format: object = NULL Create a compiled struct object. @@ -1830,8 +1830,13 @@ See help(struct) for more on format strings. static int Struct___init___impl(PyStructObject *self, PyObject *format) -/*[clinic end generated code: output=b8e80862444e92d0 input=192a4575a3dde802]*/ +/*[clinic end generated code: output=b8e80862444e92d0 input=14845875ad162992]*/ { + if (!format && !self->s_codes) { + PyErr_SetString(PyExc_TypeError, + "Struct() missing required argument 'format' (pos 1)"); + return -1; + } if (!self->init_called) { if (!self->s_codes && actual___init___impl(self, format)) { return -1; diff --git a/Modules/clinic/_struct.c.h b/Modules/clinic/_struct.c.h index e4eaadb91eb231..83ba6fdb5d0db9 100644 --- a/Modules/clinic/_struct.c.h +++ b/Modules/clinic/_struct.c.h @@ -10,7 +10,7 @@ preserve #include "pycore_modsupport.h" // _PyArg_UnpackKeywords() PyDoc_STRVAR(Struct___init____doc__, -"Struct(format)\n" +"Struct(format=)\n" "--\n" "\n" "Create a compiled struct object.\n" @@ -57,14 +57,19 @@ Struct___init__(PyObject *self, PyObject *args, PyObject *kwargs) PyObject *argsbuf[1]; PyObject * const *fastargs; Py_ssize_t nargs = PyTuple_GET_SIZE(args); - PyObject *format; + Py_ssize_t noptargs = nargs + (kwargs ? PyDict_GET_SIZE(kwargs) : 0) - 0; + PyObject *format = NULL; fastargs = _PyArg_UnpackKeywords(_PyTuple_CAST(args)->ob_item, nargs, kwargs, NULL, &_parser, - /*minpos*/ 1, /*maxpos*/ 1, /*minkw*/ 0, /*varpos*/ 0, argsbuf); + /*minpos*/ 0, /*maxpos*/ 1, /*minkw*/ 0, /*varpos*/ 0, argsbuf); if (!fastargs) { goto exit; } + if (!noptargs) { + goto skip_optional_pos; + } format = fastargs[0]; +skip_optional_pos: return_value = Struct___init___impl((PyStructObject *)self, format); exit: @@ -458,4 +463,4 @@ iter_unpack(PyObject *module, PyObject *const *args, Py_ssize_t nargs) return return_value; } -/*[clinic end generated code: output=caa7f36443e91cb9 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=29bd81cd67adda38 input=a9049054013a1b77]*/ From 628aadd9713644870a9861be2a5fe5c555a6beab Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Sat, 17 Jan 2026 03:41:37 +0300 Subject: [PATCH 06/23] + filter out Struct signature test --- Lib/test/test_inspect/test_inspect.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index b25414bea659b7..4fb2f31a14e626 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -6268,7 +6268,8 @@ def test_stat_module_has_signatures(self): def test_struct_module_has_signatures(self): import struct - self._test_module_has_signatures(struct) + unsupported_signature = {'Struct'} + self._test_module_has_signatures(struct, unsupported_signature=unsupported_signature) def test_string_module_has_signatures(self): import string From 979cc18bb183bf9a8ab90260801e1cde7de20048 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Mon, 19 Jan 2026 16:57:19 +0300 Subject: [PATCH 07/23] Update Modules/_struct.c Co-authored-by: Victor Stinner --- Modules/_struct.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/_struct.c b/Modules/_struct.c index 34e1026cd2d7e1..f87417c63d656f 100644 --- a/Modules/_struct.c +++ b/Modules/_struct.c @@ -1773,8 +1773,8 @@ actual___init___impl(PyStructObject *self, PyObject *format) Py_DECREF(format); PyErr_Format(PyExc_TypeError, "Struct() argument 1 must be a str or bytes object, " - "not %.200s", - _PyType_Name(Py_TYPE(format))); + "not %T", + format); return -1; } Py_SETREF(self->s_format, format); From db8f5f25b02039238ce1f5fb07330902609bc618 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Mon, 19 Jan 2026 17:02:26 +0300 Subject: [PATCH 08/23] address review: reformat s_new() --- Modules/_struct.c | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/Modules/_struct.c b/Modules/_struct.c index f87417c63d656f..15a378ec2a5662 100644 --- a/Modules/_struct.c +++ b/Modules/_struct.c @@ -1799,17 +1799,18 @@ s_new(PyTypeObject *type, PyObject *args, PyObject *kwds) allocfunc alloc_func = PyType_GetSlot(type, Py_tp_alloc); assert(alloc_func != NULL); self = (PyStructObject *)alloc_func(type, 0); - if (self != NULL) { - self->s_format = Py_NewRef(Py_None); - self->s_codes = NULL; - self->s_size = -1; - self->s_len = -1; - self->init_called = false; - if (PyTuple_GET_SIZE(args) > 0) { - if (actual___init___impl(self, PyTuple_GET_ITEM(args, 0))) { - Py_DECREF(self); - return NULL; - } + if (self == NULL) { + return NULL; + } + self->s_format = Py_NewRef(Py_None); + self->s_codes = NULL; + self->s_size = -1; + self->s_len = -1; + self->init_called = false; + if (PyTuple_GET_SIZE(args) > 0) { + if (actual___init___impl(self, PyTuple_GET_ITEM(args, 0))) { + Py_DECREF(self); + return NULL; } } return (PyObject *)self; From dc8cbefa50c08405ac2fb9a768512e7491dce4ab Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Mon, 19 Jan 2026 17:03:19 +0300 Subject: [PATCH 09/23] address review: actual___init___impl -> s_init --- Modules/_struct.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Modules/_struct.c b/Modules/_struct.c index 15a378ec2a5662..60d8214796f4b7 100644 --- a/Modules/_struct.c +++ b/Modules/_struct.c @@ -1759,7 +1759,7 @@ prepare_s(PyStructObject *self) } static int -actual___init___impl(PyStructObject *self, PyObject *format) +s_init(PyStructObject *self, PyObject *format) { if (PyUnicode_Check(format)) { format = PyUnicode_AsASCIIString(format); @@ -1808,7 +1808,7 @@ s_new(PyTypeObject *type, PyObject *args, PyObject *kwds) self->s_len = -1; self->init_called = false; if (PyTuple_GET_SIZE(args) > 0) { - if (actual___init___impl(self, PyTuple_GET_ITEM(args, 0))) { + if (s_init(self, PyTuple_GET_ITEM(args, 0))) { Py_DECREF(self); return NULL; } @@ -1837,7 +1837,7 @@ Struct___init___impl(PyStructObject *self, PyObject *format) return -1; } if (!self->init_called) { - if (!self->s_codes && actual___init___impl(self, format)) { + if (!self->s_codes && s_init(self, format)) { return -1; } self->init_called = true; @@ -1850,7 +1850,7 @@ Struct___init___impl(PyStructObject *self, PyObject *format) { return -1; } - return actual___init___impl(self, format); + return s_init(self, format); } static int From 12143d13f84e89f79c97702b359d026dc6c8f006 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Wed, 25 Feb 2026 12:36:07 +0300 Subject: [PATCH 10/23] Update Doc/deprecations/pending-removal-in-3.20.rst Co-authored-by: Serhiy Storchaka --- Doc/deprecations/pending-removal-in-3.20.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/deprecations/pending-removal-in-3.20.rst b/Doc/deprecations/pending-removal-in-3.20.rst index 1907758e8847d8..70eab17ae54894 100644 --- a/Doc/deprecations/pending-removal-in-3.20.rst +++ b/Doc/deprecations/pending-removal-in-3.20.rst @@ -6,7 +6,7 @@ Pending removal in Python 3.20 :meth:`~object.__init__` method on initialized :class:`~struct.Struct` objects is deprecated and will be removed in Python 3.20. - (Contributed by Sergey B Kirpichev in :gh:`78724`.) + (Contributed by Sergey B Kirpichev in :gh:`143715`.) * The ``__version__``, ``version`` and ``VERSION`` attributes have been deprecated in these standard library modules and will be removed in From 6b9b4fb501a0f333c60eed8e5b9ba5af7be353c4 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Wed, 25 Feb 2026 12:38:55 +0300 Subject: [PATCH 11/23] + rename news --- ....HZrfSA.rst => 2026-01-10-16-23-21.gh-issue-143715.HZrfSA.rst} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Misc/NEWS.d/next/Library/{2026-01-10-16-23-21.gh-issue-78724.HZrfSA.rst => 2026-01-10-16-23-21.gh-issue-143715.HZrfSA.rst} (100%) diff --git a/Misc/NEWS.d/next/Library/2026-01-10-16-23-21.gh-issue-78724.HZrfSA.rst b/Misc/NEWS.d/next/Library/2026-01-10-16-23-21.gh-issue-143715.HZrfSA.rst similarity index 100% rename from Misc/NEWS.d/next/Library/2026-01-10-16-23-21.gh-issue-78724.HZrfSA.rst rename to Misc/NEWS.d/next/Library/2026-01-10-16-23-21.gh-issue-143715.HZrfSA.rst From 79961a2e871854aa385b5694739a65c56148e5c6 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Wed, 25 Feb 2026 12:53:46 +0300 Subject: [PATCH 12/23] Apply suggestions from code review --- Doc/whatsnew/3.15.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index fde817e43fe2b4..58b8658d527f09 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -1534,7 +1534,7 @@ New deprecations :meth:`~object.__init__` method on initialized :class:`~struct.Struct` objects is deprecated and will be removed in Python 3.20. - (Contributed by Sergey B Kirpichev in :gh:`78724`.) + (Contributed by Sergey B Kirpichev in :gh:`143715`.) * ``__version__`` From 58550c0eba68361268fdac5a2ab21ee2b01878d0 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Wed, 25 Feb 2026 13:33:05 +0300 Subject: [PATCH 13/23] address review: add test --- Lib/test/test_struct.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Lib/test/test_struct.py b/Lib/test/test_struct.py index f28db07d0ea437..f316d35e43f604 100644 --- a/Lib/test/test_struct.py +++ b/Lib/test/test_struct.py @@ -802,6 +802,14 @@ def __init__(self): my_struct = MyStruct() self.assertEqual(my_struct.pack(12345), b'\x30\x39') + class MyStruct(struct.Struct): + def __new__(cls, arg): + self = super().__new__(cls, '>h') + return self + + my_struct = MyStruct(5) + self.assertEqual(my_struct.pack(123), b'\x00{') + def test_repr(self): s = struct.Struct('=i2H') self.assertEqual(repr(s), f'Struct({s.format!r})') From c815882904899f7d409a5bf02c1548640c2cf16d Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Wed, 25 Feb 2026 13:35:30 +0300 Subject: [PATCH 14/23] address review: fix for format --- Modules/_struct.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/_struct.c b/Modules/_struct.c index c82c0290ac450f..2a1a314a6dc1a3 100644 --- a/Modules/_struct.c +++ b/Modules/_struct.c @@ -1778,11 +1778,11 @@ s_init(PyStructObject *self, PyObject *format) Py_INCREF(format); } if (!PyBytes_Check(format)) { - Py_DECREF(format); PyErr_Format(PyExc_TypeError, "Struct() argument 1 must be a str or bytes object, " "not %T", format); + Py_DECREF(format); return -1; } Py_SETREF(self->s_format, format); From f1388ee2e7acce6a46933f321eb896909e8ad628 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Wed, 25 Feb 2026 13:39:41 +0300 Subject: [PATCH 15/23] address review: reformat if blocks --- Modules/_struct.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Modules/_struct.c b/Modules/_struct.c index 2a1a314a6dc1a3..929771de5621b6 100644 --- a/Modules/_struct.c +++ b/Modules/_struct.c @@ -1797,11 +1797,11 @@ s_new(PyTypeObject *type, PyObject *args, PyObject *kwds) { PyStructObject *self; - if ((PyTuple_GET_SIZE(args) != 1 || kwds) - && PyErr_WarnEx(PyExc_DeprecationWarning, - "Struct.__new__() has one positional argument", 1)) - { - return NULL; + if (PyTuple_GET_SIZE(args) != 1 || kwds) { + if (PyErr_WarnEx(PyExc_DeprecationWarning, + "Struct.__new__() has one positional argument", 1)) { + return NULL; + } } assert(type != NULL); allocfunc alloc_func = PyType_GetSlot(type, Py_tp_alloc); From 685a6c41257bad6cb87113c57e62d21623aff663 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Wed, 25 Feb 2026 13:53:39 +0300 Subject: [PATCH 16/23] address review: s_new --- Modules/_struct.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/_struct.c b/Modules/_struct.c index 929771de5621b6..41179f0204c54e 100644 --- a/Modules/_struct.c +++ b/Modules/_struct.c @@ -1797,7 +1797,7 @@ s_new(PyTypeObject *type, PyObject *args, PyObject *kwds) { PyStructObject *self; - if (PyTuple_GET_SIZE(args) != 1 || kwds) { + if (PyTuple_GET_SIZE(args) != 1) { if (PyErr_WarnEx(PyExc_DeprecationWarning, "Struct.__new__() has one positional argument", 1)) { return NULL; @@ -1815,7 +1815,7 @@ s_new(PyTypeObject *type, PyObject *args, PyObject *kwds) self->s_size = -1; self->s_len = -1; self->init_called = false; - if (PyTuple_GET_SIZE(args) > 0) { + if (PyTuple_GET_SIZE(args) == 1) { if (s_init(self, PyTuple_GET_ITEM(args, 0))) { Py_DECREF(self); return NULL; From 538d3013a55ffee69e892adf1f8c0d724ee0618d Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Thu, 26 Feb 2026 08:16:26 +0300 Subject: [PATCH 17/23] address review: add fake signature for Struct --- Lib/test/test_inspect/test_inspect.py | 3 +-- Modules/_struct.c | 3 ++- Modules/clinic/_struct.c.h | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index 43822b8a854747..4ad32c649ea83c 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -6295,8 +6295,7 @@ def test_stat_module_has_signatures(self): def test_struct_module_has_signatures(self): import struct - unsupported_signature = {'Struct'} - self._test_module_has_signatures(struct, unsupported_signature=unsupported_signature) + self._test_module_has_signatures(struct) def test_string_module_has_signatures(self): import string diff --git a/Modules/_struct.c b/Modules/_struct.c index 41179f0204c54e..9d24a69e410870 100644 --- a/Modules/_struct.c +++ b/Modules/_struct.c @@ -1825,6 +1825,7 @@ s_new(PyTypeObject *type, PyObject *args, PyObject *kwds) } /*[clinic input] +@text_signature "(format)" Struct.__init__ format: object = NULL @@ -1837,7 +1838,7 @@ to the format string. See help(struct) for more on format strings. static int Struct___init___impl(PyStructObject *self, PyObject *format) -/*[clinic end generated code: output=b8e80862444e92d0 input=6275ff3f85752dd7]*/ +/*[clinic end generated code: output=b8e80862444e92d0 input=dcf0b5a00eb0dbd9]*/ { if (!format && !self->s_codes) { PyErr_SetString(PyExc_TypeError, diff --git a/Modules/clinic/_struct.c.h b/Modules/clinic/_struct.c.h index dc430e28b367a1..a04ad6ab2cdc5e 100644 --- a/Modules/clinic/_struct.c.h +++ b/Modules/clinic/_struct.c.h @@ -10,7 +10,7 @@ preserve #include "pycore_modsupport.h" // _PyArg_UnpackKeywords() PyDoc_STRVAR(Struct___init____doc__, -"Struct(format=)\n" +"Struct(format)\n" "--\n" "\n" "Create a compiled struct object.\n" @@ -669,4 +669,4 @@ iter_unpack(PyObject *module, PyObject *const *args, Py_ssize_t nargs) return return_value; } -/*[clinic end generated code: output=2fe0686dd99b557e input=a9049054013a1b77]*/ +/*[clinic end generated code: output=e61298fe6714a259 input=a9049054013a1b77]*/ From 09d6ebdc930d39561eddd7a61adfe5cab361cf88 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Fri, 27 Feb 2026 11:24:45 +0300 Subject: [PATCH 18/23] +test new idiom --- Lib/test/test_struct.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Lib/test/test_struct.py b/Lib/test/test_struct.py index f316d35e43f604..8e00553be5def1 100644 --- a/Lib/test/test_struct.py +++ b/Lib/test/test_struct.py @@ -802,6 +802,13 @@ def __init__(self): my_struct = MyStruct() self.assertEqual(my_struct.pack(12345), b'\x30\x39') + # New way, no warnings: + class MyStruct(struct.Struct): + def __new__(cls): + return super().__new__(cls, '>h') + my_struct = MyStruct() + self.assertEqual(my_struct.pack(12345), b'\x30\x39') + class MyStruct(struct.Struct): def __new__(cls, arg): self = super().__new__(cls, '>h') From 7f0a1330c39b9794aabb8b0c091b950b06d69281 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Sat, 28 Feb 2026 08:50:09 +0300 Subject: [PATCH 19/23] Some cleanup; also add reference to issue and reverted PR --- Modules/_struct.c | 51 ++++++++++++++++++++++++++--------------------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/Modules/_struct.c b/Modules/_struct.c index 9d24a69e410870..b20dcf8519e49b 100644 --- a/Modules/_struct.c +++ b/Modules/_struct.c @@ -1766,8 +1766,10 @@ prepare_s(PyStructObject *self) return -1; } +/* This should be moved to s_new() when Struct___init__() will + be removed (see gh-143715 and gh-94532). */ static int -s_init(PyStructObject *self, PyObject *format) +s_create(PyStructObject *self, PyObject *format) { if (PyUnicode_Check(format)) { format = PyUnicode_AsASCIIString(format); @@ -1780,8 +1782,7 @@ s_init(PyStructObject *self, PyObject *format) if (!PyBytes_Check(format)) { PyErr_Format(PyExc_TypeError, "Struct() argument 1 must be a str or bytes object, " - "not %T", - format); + "not %T", format); Py_DECREF(format); return -1; } @@ -1797,12 +1798,6 @@ s_new(PyTypeObject *type, PyObject *args, PyObject *kwds) { PyStructObject *self; - if (PyTuple_GET_SIZE(args) != 1) { - if (PyErr_WarnEx(PyExc_DeprecationWarning, - "Struct.__new__() has one positional argument", 1)) { - return NULL; - } - } assert(type != NULL); allocfunc alloc_func = PyType_GetSlot(type, Py_tp_alloc); assert(alloc_func != NULL); @@ -1815,13 +1810,21 @@ s_new(PyTypeObject *type, PyObject *args, PyObject *kwds) self->s_size = -1; self->s_len = -1; self->init_called = false; - if (PyTuple_GET_SIZE(args) == 1) { - if (s_init(self, PyTuple_GET_ITEM(args, 0))) { - Py_DECREF(self); - return NULL; + if (PyTuple_GET_SIZE(args) != 1) { + if (PyErr_WarnEx(PyExc_DeprecationWarning, + "Struct.__new__() has one positional argument", 1)) { + goto err; + } + } + else { + if (s_create(self, PyTuple_GET_ITEM(args, 0))) { + goto err; } } return (PyObject *)self; +err: + Py_DECREF(self); + return NULL; } /*[clinic input] @@ -1845,21 +1848,23 @@ Struct___init___impl(PyStructObject *self, PyObject *format) "Struct() missing required argument 'format' (pos 1)"); return -1; } - if (!self->init_called) { - if (!self->s_codes && s_init(self, format)) { + if (self->init_called) { + if (self->s_codes + && PyErr_WarnEx(PyExc_DeprecationWarning, + ("Explicit call of __init__() on " + "initialized Struct() is deprecated"), 1)) + { + return -1; + } + return s_create(self, format); + } + else { + if (!self->s_codes && s_create(self, format)) { return -1; } self->init_called = true; return 0; } - if ((self->s_codes && self->init_called) - && PyErr_WarnEx(PyExc_DeprecationWarning, - ("Explicit call of __init__() on " - "initialized Struct() is deprecated"), 1)) - { - return -1; - } - return s_init(self, format); } static int From 76978206ea0358ac52e7c7860581ee60e073525f Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Sat, 28 Feb 2026 11:21:41 +0300 Subject: [PATCH 20/23] issue DeprecationWarning for implicit tp_new calls in subclasses This catch current pattern for Struct's subclassing like class MyStruct(Struct): def __init__(self): super().__init__('>h') --- Lib/test/test_struct.py | 16 ++++++++++++++++ Modules/_struct.c | 23 ++++++++++++++++++++--- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_struct.py b/Lib/test/test_struct.py index 8e00553be5def1..d4ffcf43ae68f5 100644 --- a/Lib/test/test_struct.py +++ b/Lib/test/test_struct.py @@ -817,6 +817,22 @@ def __new__(cls, arg): my_struct = MyStruct(5) self.assertEqual(my_struct.pack(123), b'\x00{') + class MyStruct(struct.Struct): + def __init__(self, *args, **kwargs): + super().__init__('>h') + + with self.assertWarns(DeprecationWarning): + my_struct = MyStruct('h') + self.assertEqual(my_struct.pack(12345), b'\x30\x39') + def test_repr(self): s = struct.Struct('=i2H') self.assertEqual(repr(s), f'Struct({s.format!r})') diff --git a/Modules/_struct.c b/Modules/_struct.c index b20dcf8519e49b..39d8827ab5f220 100644 --- a/Modules/_struct.c +++ b/Modules/_struct.c @@ -1796,7 +1796,11 @@ s_create(PyStructObject *self, PyObject *format) static PyObject * s_new(PyTypeObject *type, PyObject *args, PyObject *kwds) { + PyObject *mod = PyType_GetModuleByDef(type, &_structmodule); + _structmodulestate *state = PyModule_GetState(mod); PyStructObject *self; + bool implicit_new_call = s_new == PyType_GetSlot(type, Py_tp_new); + bool in_subclass = type != (PyTypeObject *)state->PyStructType; assert(type != NULL); allocfunc alloc_func = PyType_GetSlot(type, Py_tp_alloc); @@ -1817,8 +1821,18 @@ s_new(PyTypeObject *type, PyObject *args, PyObject *kwds) } } else { - if (s_create(self, PyTuple_GET_ITEM(args, 0))) { - goto err; + if (implicit_new_call && in_subclass) { + if (PyErr_WarnEx(PyExc_DeprecationWarning, + ("Creation of half-initialized Struct() objects" + " is deprecated, use Struct.__new__(cls, format)"), 1)) + { + goto err; + } + } + else { + if (s_create(self, PyTuple_GET_ITEM(args, 0))) { + goto err; + } } } return (PyObject *)self; @@ -1843,6 +1857,9 @@ static int Struct___init___impl(PyStructObject *self, PyObject *format) /*[clinic end generated code: output=b8e80862444e92d0 input=dcf0b5a00eb0dbd9]*/ { + bool explicit_init_call = (Struct___init__ + != PyType_GetSlot(Py_TYPE(self), Py_tp_init)); + if (!format && !self->s_codes) { PyErr_SetString(PyExc_TypeError, "Struct() missing required argument 'format' (pos 1)"); @@ -1859,7 +1876,7 @@ Struct___init___impl(PyStructObject *self, PyObject *format) return s_create(self, format); } else { - if (!self->s_codes && s_create(self, format)) { + if (explicit_init_call && s_create(self, format)) { return -1; } self->init_called = true; From 05eb292fb30d946409a8b8bbbed754378b5d80b2 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Fri, 6 Mar 2026 10:57:10 +0200 Subject: [PATCH 21/23] Fix more corner cases. --- Lib/test/test_struct.py | 149 ++++++++++++++++++++++++----- Modules/_struct.c | 191 +++++++++++++++++++++++++++---------- Modules/clinic/_struct.c.h | 71 ++++++++++++-- 3 files changed, 330 insertions(+), 81 deletions(-) diff --git a/Lib/test/test_struct.py b/Lib/test/test_struct.py index d4ffcf43ae68f5..9ba0e1c3f4de52 100644 --- a/Lib/test/test_struct.py +++ b/Lib/test/test_struct.py @@ -792,46 +792,150 @@ def test_error_propagation(fmt_str): test_error_propagation('N') test_error_propagation('n') - def test_struct_subclass_instantiation(self): + def test_custom_struct_init(self): # Regression test for https://github.com/python/cpython/issues/112358 class MyStruct(struct.Struct): - def __init__(self): + def __init__(self, *args, **kwargs): super().__init__('>h') - with self.assertWarns(DeprecationWarning): + my_struct = MyStruct('>h') + self.assertEqual(my_struct.pack(12345), b'\x30\x39') + my_struct = MyStruct(format='>h') + self.assertEqual(my_struct.pack(12345), b'\x30\x39') + + warnmsg = r"Different format arguments for __new__\(\) and __init__\(\) methods of Struct" + with self.assertWarnsRegex(FutureWarning, warnmsg): + my_struct = MyStruct('h') + self.assertEqual(my_struct.pack(12345), b'\x30\x39') + + warnmsg = r"Struct\(\) takes at most 1 argument \(2 given\)" + with self.assertWarnsRegex(DeprecationWarning, warnmsg): + my_struct = MyStruct('>h', 42) + self.assertEqual(my_struct.pack(12345), b'\x30\x39') + with self.assertWarnsRegex(DeprecationWarning, warnmsg): + my_struct = MyStruct('>h', arg=42) + self.assertEqual(my_struct.pack(12345), b'\x30\x39') + with self.assertWarnsRegex(DeprecationWarning, warnmsg): + my_struct = MyStruct('>h', format=42) + self.assertEqual(my_struct.pack(12345), b'\x30\x39') + with self.assertWarnsRegex(DeprecationWarning, warnmsg): + my_struct = MyStruct(format='>h', arg=42) + self.assertEqual(my_struct.pack(12345), b'\x30\x39') + warnmsg = r"Invalid 'format' argument for Struct\.__new__\(\): " + with self.assertWarnsRegex(DeprecationWarning, warnmsg + '.*must be'): + my_struct = MyStruct(42) + self.assertEqual(my_struct.pack(12345), b'\x30\x39') + with self.assertWarnsRegex(DeprecationWarning, warnmsg + '.*must be'): + my_struct = MyStruct(format=42) + self.assertEqual(my_struct.pack(12345), b'\x30\x39') + with self.assertWarnsRegex(DeprecationWarning, warnmsg + 'bad char'): + my_struct = MyStruct('$') + self.assertEqual(my_struct.pack(12345), b'\x30\x39') + with self.assertWarnsRegex(DeprecationWarning, warnmsg + 'bad char'): + my_struct = MyStruct(format='$') + self.assertEqual(my_struct.pack(12345), b'\x30\x39') + with self.assertWarnsRegex(DeprecationWarning, warnmsg + ".*can't encode"): + my_struct = MyStruct('\u20ac') + self.assertEqual(my_struct.pack(12345), b'\x30\x39') + with self.assertWarnsRegex(DeprecationWarning, warnmsg + ".*can't encode"): + my_struct = MyStruct(format='\u20ac') + self.assertEqual(my_struct.pack(12345), b'\x30\x39') + + def test_custom_struct_new(self): # New way, no warnings: class MyStruct(struct.Struct): - def __new__(cls): + def __new__(cls, *args, **kwargs): return super().__new__(cls, '>h') + + my_struct = MyStruct('>h') + self.assertEqual(my_struct.pack(12345), b'\x30\x39') + my_struct = MyStruct('h') - return self - - my_struct = MyStruct(5) - self.assertEqual(my_struct.pack(123), b'\x00{') - - class MyStruct(struct.Struct): - def __init__(self, *args, **kwargs): - super().__init__('>h') + def __new__(cls, newargs, initargs): + return super().__new__(cls, *newargs) + def __init__(self, newargs, initargs): + if initargs is not None: + super().__init__(*initargs) - with self.assertWarns(DeprecationWarning): - my_struct = MyStruct('h',), ('>h',)) self.assertEqual(my_struct.pack(12345), b'\x30\x39') + with self.assertRaises(TypeError): + MyStruct((), ()) + with self.assertRaises(TypeError): + MyStruct(('>h',), ()) + with self.assertRaises(TypeError): + MyStruct((), ('>h',)) + with self.assertRaises(TypeError): + MyStruct((42,), ('>h',)) + with self.assertRaises(TypeError): + with self.assertWarns(FutureWarning): + MyStruct(('>h',), (42,)) + with self.assertRaises(struct.error): + MyStruct(('$',), ('>h',)) + with self.assertRaises(struct.error): + with self.assertWarns(FutureWarning): + MyStruct(('>h',), ('$',)) + with self.assertRaises(UnicodeEncodeError): + MyStruct(('\u20ac',), ('>h',)) + with self.assertRaises(UnicodeEncodeError): + with self.assertWarns(FutureWarning): + MyStruct(('>h',), ('\u20ac',)) + with self.assertWarns(FutureWarning): + my_struct = MyStruct(('>h',), ('h') self.assertEqual(my_struct.pack(12345), b'\x30\x39') - - with self.assertWarns(DeprecationWarning): - my_struct = MyStruct('>h') + my_struct = MyStruct(format='>h') self.assertEqual(my_struct.pack(12345), b'\x30\x39') + with self.assertRaises(TypeError): + MyStruct() + with self.assertRaises(TypeError): + MyStruct(42) + with self.assertRaises(struct.error): + MyStruct('$') + with self.assertRaises(UnicodeEncodeError): + MyStruct('\u20ac') + with self.assertRaises(TypeError): + MyStruct('>h', 42) + with self.assertRaises(TypeError): + MyStruct('>h', arg=42) + with self.assertRaises(TypeError): + MyStruct(arg=42) + with self.assertRaises(TypeError): + MyStruct('>h', format='>h') def test_repr(self): s = struct.Struct('=i2H') @@ -864,8 +968,7 @@ def test_endian_table_init_subinterpreters(self): self.assertListEqual(list(results), [None] * 5) def test_operations_on_half_initialized_Struct(self): - with self.assertWarns(DeprecationWarning): - S = struct.Struct.__new__(struct.Struct) + S = struct.Struct.__new__(struct.Struct) spam = array.array('b', b' ') self.assertRaises(RuntimeError, S.iter_unpack, spam) diff --git a/Modules/_struct.c b/Modules/_struct.c index 39d8827ab5f220..281c6d0bcb1612 100644 --- a/Modules/_struct.c +++ b/Modules/_struct.c @@ -1766,10 +1766,10 @@ prepare_s(PyStructObject *self) return -1; } -/* This should be moved to s_new() when Struct___init__() will - be removed (see gh-143715 and gh-94532). */ +/* This should be moved to Struct_impl() when Struct___init__() and + * s_new() will be removed (see gh-143715 and gh-94532). */ static int -s_create(PyStructObject *self, PyObject *format) +set_format(PyStructObject *self, PyObject *format) { if (PyUnicode_Check(format)) { format = PyUnicode_AsASCIIString(format); @@ -1793,19 +1793,23 @@ s_create(PyStructObject *self, PyObject *format) return 0; } +/*[clinic input] +@classmethod +Struct.__new__ + + format: object + +Create a compiled struct object. + +Return a new Struct object which writes and reads binary data according +to the format string. See help(struct) for more on format strings. +[clinic start generated code]*/ + static PyObject * -s_new(PyTypeObject *type, PyObject *args, PyObject *kwds) +Struct_impl(PyTypeObject *type, PyObject *format) +/*[clinic end generated code: output=49468b044e334308 input=8381a9796f20f24e]*/ { - PyObject *mod = PyType_GetModuleByDef(type, &_structmodule); - _structmodulestate *state = PyModule_GetState(mod); - PyStructObject *self; - bool implicit_new_call = s_new == PyType_GetSlot(type, Py_tp_new); - bool in_subclass = type != (PyTypeObject *)state->PyStructType; - - assert(type != NULL); - allocfunc alloc_func = PyType_GetSlot(type, Py_tp_alloc); - assert(alloc_func != NULL); - self = (PyStructObject *)alloc_func(type, 0); + PyStructObject *self = (PyStructObject *)type->tp_alloc(type, 0); if (self == NULL) { return NULL; } @@ -1814,38 +1818,107 @@ s_new(PyTypeObject *type, PyObject *args, PyObject *kwds) self->s_size = -1; self->s_len = -1; self->init_called = false; - if (PyTuple_GET_SIZE(args) != 1) { - if (PyErr_WarnEx(PyExc_DeprecationWarning, - "Struct.__new__() has one positional argument", 1)) { - goto err; + if (set_format(self, format) < 0) { + Py_DECREF(self); + return NULL; + } + return (PyObject *)self; +} + + +static int +s_init(PyObject *self, PyObject *args, PyObject *kwargs); + +static PyObject * +s_new(PyTypeObject *type, PyObject *args, PyObject *kwds) +{ + if (type->tp_new != s_new) { + /* Struct.__new__() was called explicitly in a subclass' __new__(). */ + return Struct(type, args, kwds); + } + + PyObject *format = NULL; + if (PyTuple_GET_SIZE(args) == 1 && (kwds == NULL || PyDict_GET_SIZE(kwds) == 0)) { + format = Py_NewRef(PyTuple_GET_ITEM(args, 0)); + } + else if (PyTuple_GET_SIZE(args) == 0 && kwds != NULL && PyDict_GET_SIZE(kwds) == 1) { + if (PyDict_GetItemStringRef(kwds, "format", &format) < 0) { + return NULL; } } - else { - if (implicit_new_call && in_subclass) { - if (PyErr_WarnEx(PyExc_DeprecationWarning, - ("Creation of half-initialized Struct() objects" - " is deprecated, use Struct.__new__(cls, format)"), 1)) + if (format == NULL && type->tp_init != s_init) { + Py_ssize_t nargs = PyTuple_GET_SIZE(args) + (kwds ? PyDict_GET_SIZE(kwds) : 0); + if (nargs > 1) { + if (PyErr_WarnFormat(PyExc_DeprecationWarning, 1, + "Struct() takes at most 1 argument (%zd given)", nargs)) { - goto err; + Py_XDECREF(format); + return NULL; } } else { - if (s_create(self, PyTuple_GET_ITEM(args, 0))) { - goto err; + if (PyErr_WarnEx(PyExc_DeprecationWarning, + "Struct() missing required argument 'format' (pos 1)", 1)) + { + Py_XDECREF(format); + return NULL; } } } + + PyStructObject *self = (PyStructObject *)type->tp_alloc(type, 0); + if (self == NULL) { + return NULL; + } + self->s_format = Py_NewRef(Py_None); + self->s_codes = NULL; + self->s_size = -1; + self->s_len = -1; + self->init_called = false; + if (format && set_format(self, format) < 0) { + if (type->tp_init == s_init) { + /* No custom __init__() method, so __new__() should do + * all the work. */ + Py_DECREF(format); + Py_DECREF(self); + return NULL; + } + PyObject *exc = PyErr_GetRaisedException(); + Py_SETREF(self->s_format, Py_NewRef(Py_None)); + if (PyErr_WarnFormat(PyExc_DeprecationWarning, 1, + "Invalid 'format' argument for Struct.__new__(): %S", exc)) + { + Py_DECREF(exc); + Py_DECREF(format); + Py_DECREF(self); + return NULL; + } + Py_DECREF(exc); + } + Py_XDECREF(format); return (PyObject *)self; -err: - Py_DECREF(self); - return NULL; +} + +static bool +same_format(PyStructObject *s, PyObject *format) +{ + Py_ssize_t size = PyBytes_GET_SIZE(s->s_format); + const void *data = PyBytes_AS_STRING(s->s_format); + if (PyUnicode_Check(format) && PyUnicode_IS_ASCII(format)) { + return PyUnicode_GET_LENGTH(format) == size + && memcmp(PyUnicode_1BYTE_DATA(format), data, size) == 0; + } + if (PyBytes_Check(format)) { + return PyBytes_GET_SIZE(format) == size + && memcmp(PyBytes_AS_STRING(format), data, size) == 0; + } + return false; } /*[clinic input] -@text_signature "(format)" Struct.__init__ - format: object = NULL + format: object Create a compiled struct object. @@ -1855,33 +1928,51 @@ to the format string. See help(struct) for more on format strings. static int Struct___init___impl(PyStructObject *self, PyObject *format) -/*[clinic end generated code: output=b8e80862444e92d0 input=dcf0b5a00eb0dbd9]*/ +/*[clinic end generated code: output=b8e80862444e92d0 input=1af78a5f57d82cec]*/ { - bool explicit_init_call = (Struct___init__ - != PyType_GetSlot(Py_TYPE(self), Py_tp_init)); - - if (!format && !self->s_codes) { - PyErr_SetString(PyExc_TypeError, - "Struct() missing required argument 'format' (pos 1)"); - return -1; + if (self->s_format == Py_None) { + if (set_format(self, format) < 0) { + return -1; + } } - if (self->init_called) { - if (self->s_codes - && PyErr_WarnEx(PyExc_DeprecationWarning, - ("Explicit call of __init__() on " - "initialized Struct() is deprecated"), 1)) - { + else if (!same_format(self, format)) { + if (!self->init_called) { + if (PyErr_WarnEx(PyExc_FutureWarning, + "Different format arguments for __new__() " + "and __init__() methods of Struct", 1)) + { + return -1; + } + } + if (set_format(self, format) < 0) { return -1; } - return s_create(self, format); } - else { - if (explicit_init_call && s_create(self, format)) { + self->init_called = true; + return 0; +} + +static int +s_init(PyObject *self, PyObject *args, PyObject *kwargs) +{ + if (((PyStructObject *)self)->init_called) { + if (PyErr_WarnEx(PyExc_DeprecationWarning, + "Explicit call of __init__() on " + "initialized Struct is deprecated", 1)) + { return -1; } - self->init_called = true; + } + else if (Py_TYPE(self)->tp_init == s_init + && ((PyStructObject *)self)->s_format != Py_None) + { + /* Struct.__init__() was called implicitly. + * __new__() already did all the work. */ + ((PyStructObject *)self)->init_called = true; return 0; } + /* Struct.__init__() was called explicitly. */ + return Struct___init__(self, args, kwargs); } static int @@ -2495,7 +2586,7 @@ static PyType_Slot PyStructType_slots[] = { {Py_tp_methods, s_methods}, {Py_tp_members, s_members}, {Py_tp_getset, s_getsetlist}, - {Py_tp_init, Struct___init__}, + {Py_tp_init, s_init}, {Py_tp_new, s_new}, {0, 0}, }; diff --git a/Modules/clinic/_struct.c.h b/Modules/clinic/_struct.c.h index a04ad6ab2cdc5e..e75698e3ed00cc 100644 --- a/Modules/clinic/_struct.c.h +++ b/Modules/clinic/_struct.c.h @@ -9,6 +9,66 @@ preserve #include "pycore_abstract.h" // _PyNumber_Index() #include "pycore_modsupport.h" // _PyArg_UnpackKeywords() +PyDoc_STRVAR(Struct__doc__, +"Struct(format)\n" +"--\n" +"\n" +"Create a compiled struct object.\n" +"\n" +"Return a new Struct object which writes and reads binary data according\n" +"to the format string. See help(struct) for more on format strings."); + +static PyObject * +Struct_impl(PyTypeObject *type, PyObject *format); + +static PyObject * +Struct(PyTypeObject *type, PyObject *args, PyObject *kwargs) +{ + PyObject *return_value = NULL; + #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) + + #define NUM_KEYWORDS 1 + static struct { + PyGC_Head _this_is_not_used; + PyObject_VAR_HEAD + Py_hash_t ob_hash; + PyObject *ob_item[NUM_KEYWORDS]; + } _kwtuple = { + .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) + .ob_hash = -1, + .ob_item = { &_Py_ID(format), }, + }; + #undef NUM_KEYWORDS + #define KWTUPLE (&_kwtuple.ob_base.ob_base) + + #else // !Py_BUILD_CORE + # define KWTUPLE NULL + #endif // !Py_BUILD_CORE + + static const char * const _keywords[] = {"format", NULL}; + static _PyArg_Parser _parser = { + .keywords = _keywords, + .fname = "Struct", + .kwtuple = KWTUPLE, + }; + #undef KWTUPLE + PyObject *argsbuf[1]; + PyObject * const *fastargs; + Py_ssize_t nargs = PyTuple_GET_SIZE(args); + PyObject *format; + + fastargs = _PyArg_UnpackKeywords(_PyTuple_CAST(args)->ob_item, nargs, kwargs, NULL, &_parser, + /*minpos*/ 1, /*maxpos*/ 1, /*minkw*/ 0, /*varpos*/ 0, argsbuf); + if (!fastargs) { + goto exit; + } + format = fastargs[0]; + return_value = Struct_impl(type, format); + +exit: + return return_value; +} + PyDoc_STRVAR(Struct___init____doc__, "Struct(format)\n" "--\n" @@ -55,19 +115,14 @@ Struct___init__(PyObject *self, PyObject *args, PyObject *kwargs) PyObject *argsbuf[1]; PyObject * const *fastargs; Py_ssize_t nargs = PyTuple_GET_SIZE(args); - Py_ssize_t noptargs = nargs + (kwargs ? PyDict_GET_SIZE(kwargs) : 0) - 0; - PyObject *format = NULL; + PyObject *format; fastargs = _PyArg_UnpackKeywords(_PyTuple_CAST(args)->ob_item, nargs, kwargs, NULL, &_parser, - /*minpos*/ 0, /*maxpos*/ 1, /*minkw*/ 0, /*varpos*/ 0, argsbuf); + /*minpos*/ 1, /*maxpos*/ 1, /*minkw*/ 0, /*varpos*/ 0, argsbuf); if (!fastargs) { goto exit; } - if (!noptargs) { - goto skip_optional_pos; - } format = fastargs[0]; -skip_optional_pos: return_value = Struct___init___impl((PyStructObject *)self, format); exit: @@ -669,4 +724,4 @@ iter_unpack(PyObject *module, PyObject *const *args, Py_ssize_t nargs) return return_value; } -/*[clinic end generated code: output=e61298fe6714a259 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=0f417d43a2a387c8 input=a9049054013a1b77]*/ From d9c0488a53fad9ecb1375d1f071fa7b1fe7efb10 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sat, 7 Mar 2026 14:05:50 +0200 Subject: [PATCH 22/23] Update credicts. --- Doc/deprecations/pending-removal-in-3.20.rst | 2 +- Doc/whatsnew/3.15.rst | 2 +- .../next/Library/2026-01-10-16-23-21.gh-issue-143715.HZrfSA.rst | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Doc/deprecations/pending-removal-in-3.20.rst b/Doc/deprecations/pending-removal-in-3.20.rst index 1b12f0ad330ccc..d59f6881adf00b 100644 --- a/Doc/deprecations/pending-removal-in-3.20.rst +++ b/Doc/deprecations/pending-removal-in-3.20.rst @@ -6,7 +6,7 @@ Pending removal in Python 3.20 :meth:`~object.__init__` method on initialized :class:`~struct.Struct` objects is deprecated and will be removed in Python 3.20. - (Contributed by Sergey B Kirpichev in :gh:`143715`.) + (Contributed by Sergey B Kirpichev and Serhiy Storchaka in :gh:`143715`.) * The ``__version__``, ``version`` and ``VERSION`` attributes have been deprecated in these standard library modules and will be removed in diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 7cec0a7b0c2db2..0dc0024b7848ab 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -1534,7 +1534,7 @@ New deprecations :meth:`~object.__init__` method on initialized :class:`~struct.Struct` objects is deprecated and will be removed in Python 3.20. - (Contributed by Sergey B Kirpichev in :gh:`143715`.) + (Contributed by Sergey B Kirpichev and Serhiy Storchaka in :gh:`143715`.) * ``__version__`` diff --git a/Misc/NEWS.d/next/Library/2026-01-10-16-23-21.gh-issue-143715.HZrfSA.rst b/Misc/NEWS.d/next/Library/2026-01-10-16-23-21.gh-issue-143715.HZrfSA.rst index 93c3f6f74f207b..90aae6bee835f0 100644 --- a/Misc/NEWS.d/next/Library/2026-01-10-16-23-21.gh-issue-143715.HZrfSA.rst +++ b/Misc/NEWS.d/next/Library/2026-01-10-16-23-21.gh-issue-143715.HZrfSA.rst @@ -1,3 +1,3 @@ Calling the ``Struct.__new__()`` without required argument now is deprecated. Calling :meth:`~object.__init__` method on initialized :class:`~struct.Struct` -objects is deprecated. Patch by Sergey B Kirpichev. +objects is deprecated. From 3de045be4976fdfc72a2e607a14d66f5e7d5a957 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sat, 7 Mar 2026 14:10:03 +0200 Subject: [PATCH 23/23] Fix format for the __sizeof__ test. --- Lib/test/test_struct.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_struct.py b/Lib/test/test_struct.py index 9ba0e1c3f4de52..611659a6a0e3e6 100644 --- a/Lib/test/test_struct.py +++ b/Lib/test/test_struct.py @@ -594,7 +594,7 @@ def test_Struct_reinitialization(self): def check_sizeof(self, format_str, number_of_codes): # The size of 'PyStructObject' - totalsize = support.calcobjsize('2n3P1?') + totalsize = support.calcobjsize('2n3P?0P') # The size taken up by the 'formatcode' dynamic array totalsize += struct.calcsize('P3n0P') * (number_of_codes + 1) support.check_sizeof(self, struct.Struct(format_str), totalsize)