Repository navigation
Make struct.Struct() really immutable #143715
Description
Bug report
Bug description:
The Struct constructor permits creation of half-initialized Struct's, e.g.:
>>> from _struct import Struct
>>> s = Struct.__new__(Struct)
>>> s.unpack_from(b'boo!') # this might be a crash!
Traceback (most recent call last):
File "<python-input-2>", line 1, in <module>
s.unpack_from(b'boo!')
~~~~~~~~~~~~~^^^^^^^^^
SystemError: Objects/tupleobject.c:40: bad argument to internal function
>>> s = Struct.__new__(Struct, 1, 2, 3) # anything is accepted
>>> s.unpack_from(b'boo!')
Traceback (most recent call last):
File "<python-input-6>", line 1, in <module>
s.unpack_from(b'boo!')
~~~~~~~~~~~~~^^^^^^^^^
SystemError: Objects/tupleobject.c:40: bad argument to internal functionc.f.:
>>> int.__new__(int, 1, 2, 3)
Traceback (most recent call last):
File "<python-input-7>", line 1, in <module>
int.__new__(int, 1, 2,3 )
~~~~~~~~~~~^^^^^^^^^^^^^^
TypeError: int() takes at most 2 arguments (3 given)
The Struct.__new__() dunder handles only memory allocation, the rest goes to the Struct.__init__(). That doesn't make sense for immutable type (which Struct() pretend to be in fact) and introduce a number of issues, e.g.:
- _struct.Struct: calling functions without calling __init__ results in SystemError #78724
- assertion failures and a crash when using an uninitialized struct.Struct object #75960
- Use-after-free in
s_pack_internalvia re-entrant__bool__#143379
The proper way to fix all this, probably, is moving all initialization logic to the Struct.__new__() dunder:
It is more convenient to initialize the Struct instance in
__new__than in__init__, and it makes sense, since Struct instances are cached and therefore can be considered immutable like ints or tuples. But the possibility of creating subclasses and the existence of subclasses in the wild makes this a breaking change.
Originally posted by @serhiy-storchaka in #112358
From docs:
A good rule of thumb is that for immutable types, all initialization should take place in tp_new, while for mutable types, most initialization should be deferred to tp_init.
This was done in #94532, which then was reverted due to introduced breackage (#112358).
I propose:
- deprecate repeated calls of the
Struct.__init__()on initialized Struct (will be a no-op eventually) - move all initialization logic to
Struct.__new__(), makeself.__init__()a no-op if__new__()got one argument - deprecate calls of
Struct.__new__()without required argument.
The Struct.__init__() dunder method will be removed in the CPython 3.20. I suggest to close all opened referenced above issues as duplicates of this one.
CPython versions tested on:
CPython main branch
Operating systems tested on:
No response
Linked PRs
Metadata
Metadata
Assignees
Labels
Projects
- StatusShow more project fieldsDone
Activity
skirpichev commentedon Jan 12, 2026
CC @serhiy-storchaka
serhiy-storchaka commentedon Jan 12, 2026
To arguments about which Struct should be considered immutable:
__init__()mutates a Struct objectInitializing the Struct object in
__init__()caused #78724. The most probable way to encounter that bug not when the Struct object was created by callingStruct.__new__(), but whensuper().__init__()was omitted in a Struct subclass.The previous attempt to fix this issue failed because people write the code like:
If the constructor would be implemented in
__new__(), they would write it differently:This is not compatible with the current implementation, and the current way is not compatible with initialization made in
__new__(). If we are going to move the initialization code from__init__()to__new__(), we have to design the transition way which would cause less pain to users.super().__init__()in the subclass's__init__()) should rather fail than silently produce incorrect result in future Python versions. And if it fails, there should be a deprecation warning in the transition period.This is not easy.
There is other way to make Struct practically immutable -- forbid repeated calls of
Struct.__init__(). Between the calls ofStruct.__new__()andStruct.__init__(), the object is in a "not initialized" state and should not be used. AfterStruct.__init__()was called, it becomes immutable, andStruct.__init__()cannot be called repeatedly. This will solve the problem that Struct can be mutated.serhiy-storchaka commentedon Jan 12, 2026
I am planning to also deprecate calls of
__init__()method on initialized Picler and Unpickler objects in_pickle. They are not immutable, strictly speaking, but reinitialization during the call of their method can have bad consequences.skirpichev commentedon Jan 13, 2026
This emits a warning in my pr:
This is more easy to achieve (and done in my pr):
I think both goals are reachable, but not without hacks. I did
formatargument optional in the__init__(). If it's not provided and the object is not initialized in the__new__()- the TypeError raised, as before. In this way - the second idoom should work after merging PR without warnings:>>> MyStruct2() MyStruct2('>h')s_pack_internalvia re-entrant__bool__#143379 as a duplicate of this issueserhiy-storchaka commentedon Feb 25, 2026
The following code also should work without warnings:
Both
Struct.__new__andStruct.__init__can be called explicitly (in the overwritten__new__or__init__) or implicitly (when__new__or__init__are not defined in a Struct subclass). We can checktp_newandtp_initof the current class. If they are the same as in the base class, they were called implicitly. IfStruct.__init__is called implicitly, it should be virtually no-op. We can only check thatStruct.__init__was not called multiple times.gh-143715: Deprecate incomplete initialization of struct.Struct() (GH…
pythongh-143715: Deprecate incomplete initialization of struct.Struct…