Repository navigation
Make struct.Struct() really immutable #143715
Description
Activity
- addedtype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or errorextension-modulesC modules in the Modules dirC modules in the Modules dir
on Jan 12, 2026 To arguments about which Struct should be considered immutable:
- Currently, only
__init__()mutates a Struct object - There is a global cache for Struct. Mutating the cached Struct object breaks the cache.
Initializing 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:
class MyStruct(struct.Struct): def __init__(self): super().__init__('>h')
If the constructor would be implemented in
__new__(), they would write it differently:class MyStruct(struct.Struct): def __new__(cls): self = super().__new__(cls, '>h') return self
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.- The current idiom (calling
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. - It should be possible to write a user code compatible with future Python versions which works without warnings during transitional period.
- It should be possible to write a user code compatible with future Python versions and with old Python versions which works without warnings during transitional period.
- Preferably, such compatible code should not add much overhead in comparison with the code written only for future Python versions.
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.Reacted by Petr Viktorin- Currently, only
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.The current idiom (calling
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 emits a warning in my pr:
>>> import struct ... # Old idiom: ... class MyStruct(struct.Struct): ... def __init__(self): ... super().__init__('>h') ... # New idiom: ... class MyStruct2(struct.Struct): ... def __new__(cls): ... self = super().__new__(cls, '>h') ... return self ... >>> MyStruct() <python-input-2>:1: DeprecationWarning: Struct.__new__() has one positional argument MyStruct('>h')
There is other way to make Struct practically immutable -- forbid repeated calls of
Struct.__init__().This is more easy to achieve (and done in my pr):
>>> s = struct.Struct('i') >>> s.__init__('ii') <python-input-7>:1: DeprecationWarning: Explicit call of __init__() on initialized Struct() is deprecated >>> s.pack(1, 2) b'\x01\x00\x00\x00\x02\x00\x00\x00'
It should be possible to write a user code compatible with future Python versions which works without warnings during transitional period.
It should be possible to write a user code compatible with future Python versions and with old Python versions which works without warnings during transitional period.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')- marked Use-after-free in
s_pack_internalvia re-entrant__bool__#143379 as a duplicate of this issueon Jan 15, 2026 The following code also should work without warnings:
class MyStruct(struct.Struct): def __new__(cls, arg): self = super().__new__(cls, '>h') return self MyStruct(5)
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.- added a commit that references this issue
on Mar 12, 2026 - marked struct.Struct crashes (seg fault) under concurrent re-initialization and unpack in free-threading builds #146020 as a duplicate of this issue
on Mar 16, 2026 - moved this to Todo in Struct, memoryview and array issues 🏗️
on Jul 13, 2026 - moved this from Todo to Done in Struct, memoryview and array issues 🏗️
on Jul 13, 2026
Metadata
Metadata
Assignees
Labels
Projects
- StatusShow more project fieldsDone
Bug report
Bug description:
The Struct constructor permits creation of half-initialized Struct's, e.g.:
c.f.:
The
Struct.__new__()dunder handles only memory allocation, the rest goes to theStruct.__init__(). That doesn't make sense for immutable type (whichStruct()pretend to be in fact) and introduce a number of issues, e.g.:s_pack_internalvia re-entrant__bool__#143379The proper way to fix all this, probably, is moving all initialization logic to the
Struct.__new__()dunder:Originally posted by @serhiy-storchaka in #112358
From docs:
This was done in #94532, which then was reverted due to introduced breackage (#112358).
I propose:
Struct.__init__()on initialized Struct (will be a no-op eventually)Struct.__new__(), makeself.__init__()a no-op if__new__()got one argumentStruct.__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