You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
3.14 regression: slot dataclasses classes leak original class #135228
Originally I ran into this issue with slotted attrs classes but the core problem is the same. Since slotness cannot be added to a class, dataclasses and attrs classes create a class copy with slots instead. The old class used to hang around until a GC collection happened (guess there are some reference cycles, but I never investigated thoroughly).
On 3.14, a full GC collection does not clean up the original class, causing a leak. Apart from leaking, in case of subclassing the original class will remain in the list of the parent .__subclasses__(), causing an issue.
I think this is because the annotate function for the class holds on to the original class namespace, and that namespace dict contains the descriptors B.__dict__ and B.__weakref__ that refer back to the original class. Not too sure how to fix that yet.
This seems hard to avoid, maybe we should just live with the change? The reason is that the annotation scope needs to hold a reference to the actual class dict (for reasons explained in https://jellezijlstra.github.io/pep695#class-scopes) and the class dict contains two descriptors, for __dict__ and __weakref__, that contain references back to the class object.
In Python 3.12 and 3.13 this behavior is already observable if you use a type alias or TypeVar bound in the dataclass, with one of these class definitions:
@dataclass(slots=True)classB:
deff[T: int](self, x: T) ->T: ...
b: int@dataclass(slots=True)classB:
type T=intb: int
Both of these fail the assertion in the post above.
I put up a change in #135230 that fixes this issue, but messing with the descriptors feels like it's probably going to break something else.
This will break a piece of cattrs and anyone using __subclasses__() with slotted dataclasses and attrs classes. Maybe we can work around it in the __subclassess__() implementation somehow?
This function adds methods __delattr__ and __setattr__ to the function builder, but it also updates locals of function builder with the reference of the class.
These locals are passed to function __create_fn__ that creates other functions for the class.
__delattr__ and __setattr__ reference the class through closure; hence, making 2 circular references.
# f will be None in the case of a property where not all of
# fget, fset, and fdel are used. Nothing to do in that case.
returnFalse
try:
idx=f.__code__.co_freevars.index("__class__")
exceptValueError:
# This function doesn't reference __class__, so nothing to do.
returnFalse
# Fix the cell to point to the new class, if it's already pointing
# at the old class. I'm not convinced that the "is oldcls" test
# is needed, but other than performance can't hurt.
closure=f.__closure__[idx]
ifclosure.cell_contentsisoldcls:
closure.cell_contents=newcls
returnTrue
returnFalse
But it doesn't do this for __setattr__ and __delattr__ because they reference class via cls not via __class__.
__delattr__ and __setattr__ reference the class through closure; hence, making 2 circular references
I was wrong in my last comment. The two references that make the old class uncollectable are the references from __delattr__ and __setattr__ of the new class because closure cells for those are never updated!
Given that the solution should be simple: just use __class__ instead of cls in __delattr__ and __setattr__ closure and those cells would be updated via _update_func_cell_for__class__. Local tests showed this change solves the leak.
All cases pass on the current tip of main and on the 3.14 branch, so I'm going to say the issue is fixed. If there are any more similar edge cases, probably better to open a new issue.
Just out of curiosity, since we're now delving into C-land, would it be possible to have a C function that would rework a class in-place to make it a slotted class? I feel like it would solve a number of issues at the root.
yes that is probably a better long-term solution. My understanding is that it's possible in C to change "slottedness" after the fact, but it's safe only if no instances of the class have been created yet, and proving that condition is not trivial. Maybe there's a way but it's not something we can do for 3.14 at this point.
The approach of replacing a class with another class indeed seems flawed.
However, if class building were done properly through a metaclass rather than a decorator, this would not be a problem in the first place, since a metaclass lets you hook into the class creation process itself.
Asking for new means to change slottedness after a class has been created sounds like trying to work around the consequences of an unfortunate API design choice.
On the other hand, given that dataclasses are now the way they are, it might be the best way forward...
Activity
JelleZijlstra commentedon Jun 7, 2025
I think this is because the annotate function for the class holds on to the original class namespace, and that namespace dict contains the descriptors
B.__dict__andB.__weakref__that refer back to the original class. Not too sure how to fix that yet.pythongh-135228: Break reference cycle between class and descriptors
JelleZijlstra commentedon Jun 7, 2025
This seems hard to avoid, maybe we should just live with the change? The reason is that the annotation scope needs to hold a reference to the actual class dict (for reasons explained in https://jellezijlstra.github.io/pep695#class-scopes) and the class dict contains two descriptors, for
__dict__and__weakref__, that contain references back to the class object.In Python 3.12 and 3.13 this behavior is already observable if you use a type alias or TypeVar bound in the dataclass, with one of these class definitions:
Both of these fail the assertion in the post above.
I put up a change in #135230 that fixes this issue, but messing with the descriptors feels like it's probably going to break something else.
Tinche commentedon Jun 28, 2025
@JelleZijlstra I think you're right that the leak itself is not a big deal, and we can just live with it.
What is a bigger deal is this:
This will break a piece of cattrs and anyone using
__subclasses__()with slotted dataclasses and attrs classes. Maybe we can work around it in the__subclassess__()implementation somehow?gpshead commentedon Jul 20, 2025
release blocker status just to help force consideration. It may wind up deferred.
pythongh-135228: When @DataClass(slots=True) replaces a dataclass, ma…
54 remaining items
[3.14] pythongh-135228: When @DataClass(slots=True) replaces a datacl…
[3.14] Revert "[3.14] pythongh-135228: When @DataClass(slots=True) re…
[3.14] pythongh-135228: When @DataClass(slots=True) replaces a datacl…
Prometheus3375 commentedon Jan 15, 2026
Glad I found this issue. The original example passes on 3.14.2. However, I've just found a new one.
The difference is
frozen=Truein dataclass call.frozenenables calling function_frozen_get_del_attr.__delattr__and__setattr__to the function builder, but it also updates locals of function builder with the reference of the class.__create_fn__that creates other functions for the class.__delattr__and__setattr__reference the class through closure; hence, making 2 circular references.cpython/Lib/dataclasses.py
Lines 728 to 748 in c461aa9
I believe this can be fixed by using a weak reference to the class. In such a case I can make PR.
Prometheus3375 commentedon Jan 19, 2026
I investigated this further and realized that there is a whole function that updates closures for slotted class.
cpython/Lib/dataclasses.py
Lines 1283 to 1301 in c461aa9
But it doesn't do this for
__setattr__and__delattr__because they reference class viaclsnot via__class__.I was wrong in my last comment. The two references that make the old class uncollectable are the references from
__delattr__and__setattr__of the new class because closure cells for those are never updated!Given that the solution should be simple: just use
__class__instead ofclsin__delattr__and__setattr__closure and those cells would be updated via_update_func_cell_for__class__. Local tests showed this change solves the leak.[3.13] pythongh-105936: Properly update closure cells for `__setattr_…
JelleZijlstra commentedon Apr 24, 2026
I had Codex write test cases for all the variants discussed in this issue:
All cases pass on the current tip of main and on the 3.14 branch, so I'm going to say the issue is fixed. If there are any more similar edge cases, probably better to open a new issue.
[3.13] gh-105936: Properly update closure cells for __setattr__ and _…
fix(toolchain): pin Python floor to 3.14.6 for the frozen+slots datac…
fix(toolchain): pin Python floor to 3.14.6 for the frozen+slots datac…
fix(toolchain): pin Python floor to 3.14.6 for the frozen+slots datac…
fix(toolchain): pin Python floor to 3.14.6 for the frozen+slots datac…
mara004 commentedon Aug 13, 2026
The approach of replacing a class with another class indeed seems flawed.
However, if class building were done properly through a metaclass rather than a decorator, this would not be a problem in the first place, since a metaclass lets you hook into the class creation process itself.
Asking for new means to change slottedness after a class has been created sounds like trying to work around the consequences of an unfortunate API design choice.
On the other hand, given that dataclasses are now the way they are, it might be the best way forward...