Skip to content

Frozen dataclasses with slots raise TypeError #90055

Description

@treyhunner
BPO 45897
Nosy @ericvsmith, @treyhunner, @AlexWaygood
PRs
  • bpo-45897: Fix frozen-slotted dataclass bug #29895
  • Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.

    Show more details

    GitHub fields:

    assignee = 'https://github.com/ericvsmith'
    closed_at = None
    created_at = <Date 2021-11-25.01:53:51.202>
    labels = ['type-bug', 'library', '3.10', '3.11']
    title = 'Frozen dataclasses with slots raise TypeError'
    updated_at = <Date 2021-12-02.17:20:40.230>
    user = 'https://github.com/treyhunner'

    bugs.python.org fields:

    activity = <Date 2021-12-02.17:20:40.230>
    actor = 'AlexWaygood'
    assignee = 'eric.smith'
    closed = False
    closed_date = None
    closer = None
    components = ['Library (Lib)']
    creation = <Date 2021-11-25.01:53:51.202>
    creator = 'trey'
    dependencies = []
    files = []
    hgrepos = []
    issue_num = 45897
    keywords = ['patch']
    message_count = 4.0
    messages = ['406973', '406974', '406995', '407540']
    nosy_count = 3.0
    nosy_names = ['eric.smith', 'trey', 'AlexWaygood']
    pr_nums = ['29895']
    priority = 'normal'
    resolution = None
    stage = 'patch review'
    status = 'open'
    superseder = None
    type = 'behavior'
    url = 'https://bugs.python.org/issue45897'
    versions = ['Python 3.10', 'Python 3.11']

    Activity

    1. treyhunner commented on Nov 25, 2021

      @treyhunner
      MemberAuthor

      When making a dataclass with slots=True and frozen=True, assigning to an invalid attribute raises a TypeError rather than a FrozenInstanceError:

      >>> from dataclasses import dataclass
      >>> @dataclass(frozen=True, slots=True)
      ... class Vector:
      ...     x: float
      ...     y: float
      ...     z: float
      ...
      >>> v = Vector(1, 2, 3)
      >>> v.a = 4
      Traceback (most recent call last):
        File "<stdin>", line 1, in <module>
        File "<string>", line 5, in __setattr__
      TypeError: super(type, obj): obj must be an instance or subtype of type
    2. AlexWaygood commented on Nov 25, 2021

      @AlexWaygood
      Member

      This looks to be due to the fact that slots=True leads to the creation of an entirely new class (see line 1102), meaning that in the super(cls, self) calls in lines 611 and 618 (in the _frozen_get_del_attr function, responsible for generating __setattr__ and __delattr__ methods), self is no longer an instance of cls.

      I believe this can be fixed by tweaking _frozen_get_del_attr so that cls in the generated __setattr__ and __delattr__ methods is dynamically computed (cls = type(self)), rather than read from a closure, as is currently the case.

    3. added
      stdlibStandard Library Python modules in the Lib/ directory
      on Nov 25, 2021
    4. ericvsmith commented on Nov 25, 2021

      @ericvsmith
      Member

      I think the error should be AttributeError, which is what you'd get if the class weren't frozen.

    5. AlexWaygood commented on Dec 2, 2021

      @AlexWaygood
      Member

      You get the same error if you subclass a frozen dataclass, then try to set an attribute that is not one of the superclass's __slots__:

      >>> @dataclass(slots=True, frozen=True)
      ... class Point:
      ...     x: int
      ...     y: int
      ... 
      ...     
      >>> class Subclass(Point): pass
      ... 
      >>> s = Subclass(1, 2)
      >>> s.z = 5
      Traceback (most recent call last):
        File "<pyshell#15>", line 1, in <module>
          s.z = 5
        File "<string>", line 7, in __setattr__
      TypeError: super(type, obj): obj must be an instance or subtype of type
      
    6. transferred this issue fromon Apr 10, 2022
    7. MojoVampire commented on May 3, 2022

      @MojoVampire
      Contributor

      I believe this can be fixed by tweaking _frozen_get_del_attr so that cls in the generated __setattr__ and __delattr__ methods is dynamically computed (cls = type(self)), rather than read from a closure, as is currently the case.

      It looks like this is not the approach you took, but to be clear, using type(self) for the cls argument to super() does not work. It seems to work when you just have the one class, then becomes an infinite recursion error when you subclass the class (an empty stub subclass is all you need) and try to call the method in question on an instance of the subclass. It needs to be read from the closure because the cls in question must be a definition-time association (the method itself is inherently linked to the class it was defined in); at runtime, there's no other source that will tell you where you were defined (it's why Python hides a __class__ closure scope variable in every method that references __class__ or super, just in case).

    8. AlexWaygood commented on May 3, 2022

      @AlexWaygood
      Member

      I believe this can be fixed by tweaking _frozen_get_del_attr so that cls in the generated __setattr__ and __delattr__ methods is dynamically computed (cls = type(self)), rather than read from a closure, as is currently the case.

      It looks like this is not the approach you took, but to be clear, using type(self) for the cls argument to super() does not work. It seems to work when you just have the one class, then becomes an infinite recursion error when you subclass the class (an empty stub subclass is all you need) and try to call the method in question on an instance of the subclass.

      Yes, this is why this is not the approach I took :) thanks for updating the issue though! I forgot that I'd left that comment

    9. ericvsmith commented on Oct 3, 2022

      @ericvsmith
      Member

      Okay, I'm finally getting a chance to look at this. A few things:

      • I don't think we can change this in a minor release (3.10 or 3.11, at this point).
      • Is this even worth fixing? If the code had always raised AttributeError, I would be happy with it. But is the cost of disruption worth it? I realize that very few people probably actually catching this exception, and the value here is at the REPL to give a better error message. But still, there's probably more than zero people this would affect.
    10. ericvsmith commented on Oct 4, 2022

      @ericvsmith
      Member

      I'm going to close this. If someone really feels strongly that the exception needs to change, please open a discussion on discuss.python.org. We can re-open this issue if a discussion there produces some consensus.

    11. sndrtj commented on Aug 22, 2026

      @sndrtj

      As a workaround, setting __slots__ manually gives the normal, expected exception.

      E.g.

      @dataclasses.dataclass(frozen=True)
      class Point:
          __slots__ = ("x", "y")
          x: int
          y: int
      
      point = Point(x=1, y=1)
      
      point.z = 1

      raises dataclasses.FrozenInstanceError: cannot assign to field 'z' instead of TypeError: super(type, obj): obj (instance of Point) is not an instance or subtype of type (Point).

    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

    Metadata

    Metadata

    Assignees

    Labels

    3.10 (EOL)end of life3.11only security fixesstdlibStandard Library Python modules in the Lib/ directorytype-bugAn unexpected behavior, bug, or error

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions