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/25] 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/25] 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/25] 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/25] 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/25] 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/25] + 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/25] 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/25] 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/25] 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/25] 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/25] + 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] +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/25] 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/25] 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 8a1fa9a3c89682d7c53d9fdf19823d8f94ad17e0 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Mon, 2 Mar 2026 16:42:57 +0300 Subject: [PATCH 21/25] Apply suggestions from code review Co-authored-by: Victor Stinner --- Lib/test/test_struct.py | 2 +- .../next/Library/2026-01-10-16-23-21.gh-issue-143715.HZrfSA.rst | 2 +- Modules/_struct.c | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_struct.py b/Lib/test/test_struct.py index d4ffcf43ae68f5..037a2b6507e3ca 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?') # 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/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..6c9b9a8a355d2b 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. +:mod:`struct`: 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 39d8827ab5f220..a2a5bb3be627b0 100644 --- a/Modules/_struct.c +++ b/Modules/_struct.c @@ -1809,7 +1809,7 @@ s_new(PyTypeObject *type, PyObject *args, PyObject *kwds) if (self == NULL) { return NULL; } - self->s_format = Py_NewRef(Py_None); + self->s_format = Py_None; self->s_codes = NULL; self->s_size = -1; self->s_len = -1; From d9ed03279b27f53392709dd42ff5cde962fe0711 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Mon, 2 Mar 2026 16:46:33 +0300 Subject: [PATCH 22/25] address review: fix byte order in test_Struct_reinitialization() --- Lib/test/test_struct.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_struct.py b/Lib/test/test_struct.py index 037a2b6507e3ca..d831304253d7cf 100644 --- a/Lib/test/test_struct.py +++ b/Lib/test/test_struct.py @@ -586,8 +586,8 @@ def test_Struct_reinitialization(self): # when running with regrtest -L. s = struct.Struct('i') with self.assertWarns(DeprecationWarning): - s.__init__('ii') - self.assertEqual(s.format, 'ii') + s.__init__(' Date: Mon, 2 Mar 2026 16:49:23 +0300 Subject: [PATCH 23/25] address review: PEP 7 --- Modules/_struct.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Modules/_struct.c b/Modules/_struct.c index a2a5bb3be627b0..bf6eeb5213433c 100644 --- a/Modules/_struct.c +++ b/Modules/_struct.c @@ -1773,8 +1773,9 @@ s_create(PyStructObject *self, PyObject *format) { if (PyUnicode_Check(format)) { format = PyUnicode_AsASCIIString(format); - if (format == NULL) + if (format == NULL) { return -1; + } } else { Py_INCREF(format); From d90dfc2ec2829bacaf93cda9af126167254064c2 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Mon, 2 Mar 2026 17:23:05 +0300 Subject: [PATCH 24/25] address review: test format attribute instead of pack() --- Lib/test/test_struct.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/Lib/test/test_struct.py b/Lib/test/test_struct.py index d831304253d7cf..e5fac51624b245 100644 --- a/Lib/test/test_struct.py +++ b/Lib/test/test_struct.py @@ -588,9 +588,6 @@ def test_Struct_reinitialization(self): with self.assertWarns(DeprecationWarning): s.__init__('h') # 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') + self.assertEqual(my_struct.format, '>h') class MyStruct(struct.Struct): def __new__(cls, arg): @@ -815,7 +812,7 @@ def __new__(cls, arg): return self my_struct = MyStruct(5) - self.assertEqual(my_struct.pack(123), b'\x00{') + self.assertEqual(my_struct.format, '>h') class MyStruct(struct.Struct): def __init__(self, *args, **kwargs): @@ -823,15 +820,15 @@ def __init__(self, *args, **kwargs): with self.assertWarns(DeprecationWarning): my_struct = MyStruct('h') with self.assertWarns(DeprecationWarning): my_struct = MyStruct(5) - self.assertEqual(my_struct.pack(12345), b'\x30\x39') + self.assertEqual(my_struct.format, '>h') with self.assertWarns(DeprecationWarning): my_struct = MyStruct('>h') - self.assertEqual(my_struct.pack(12345), b'\x30\x39') + self.assertEqual(my_struct.format, '>h') def test_repr(self): s = struct.Struct('=i2H') From e1d3b7deaca3ca06c9fa9afe268500e29b61c5a0 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Mon, 2 Mar 2026 17:25:29 +0300 Subject: [PATCH 25/25] Update Modules/_struct.c Co-authored-by: Victor Stinner --- Modules/_struct.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Modules/_struct.c b/Modules/_struct.c index bf6eeb5213433c..748031ad7c7e30 100644 --- a/Modules/_struct.c +++ b/Modules/_struct.c @@ -1831,7 +1831,8 @@ s_new(PyTypeObject *type, PyObject *args, PyObject *kwds) } } else { - if (s_create(self, PyTuple_GET_ITEM(args, 0))) { + PyObject *format = PyTuple_GET_ITEM(args, 0); + if (s_create(self, format)) { goto err; } }