Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Python 3.13 regression: Recursive dataclasses fail to ==: RecursionError: maximum recursion depth exceeded #116647

Closed
hroncok opened this issue Mar 12, 2024 · 5 comments
Labels
stdlib Python modules in the Lib dir type-bug An unexpected behavior, bug, or error

Comments

@hroncok
Copy link
Contributor

hroncok commented Mar 12, 2024

Bug report

Bug description:

There is a regression in comparing recursive dataclasses for equality in Python 3.13.0 from the first alpha until at least a4.

Python 3.12

>>> from dataclasses import dataclass
>>> @dataclass
... class C:
...     recursive: object = ...
... 
>>> c1 = C()
>>> c1.recursive = c1
>>> c1 == c1
True

Python 3.13

>>> from dataclasses import dataclass
>>> @dataclass
... class C:
...     recursive: object = ...
... 
>>> c1 = C()
>>> c1.recursive = c1
>>> c1 == c1
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
    c1 == c1
  File "<string>", line 4, in __eq__
  File "<string>", line 4, in __eq__
  File "<string>", line 4, in __eq__
  [Previous line repeated 996 more times]
RecursionError: maximum recursion depth exceeded

This has started happening since 18cfc1e.

Previously, tuples were compared via ==, which skips calling __eq__ for items with the same identity. Now it skips the identity check and compares items directly with __eq__, causing RecursionError.

This breaks sip-build; hence, we cannot build pyqt5 or pyqt6 in Fedora with Python 3.13 to test the entire stack. For details, see this Fedora bugzilla.

sip-build: An internal error occurred...
Traceback (most recent call last):
  File "/usr/bin/sip-build", line 8, in <module>
    sys.exit(main())
             ^^^^^^
  File "/usr/lib64/python3.13/site-packages/sipbuild/tools/build.py", line 37, in main
    handle_exception(e)
  File "/usr/lib64/python3.13/site-packages/sipbuild/exceptions.py", line 81, in handle_exception
    raise e
  File "/usr/lib64/python3.13/site-packages/sipbuild/tools/build.py", line 34, in main
    project.build()
  File "/usr/lib64/python3.13/site-packages/sipbuild/project.py", line 245, in build
    self.builder.build()
  File "/usr/lib64/python3.13/site-packages/sipbuild/builder.py", line 48, in build
    self._generate_bindings()
  File "/usr/lib64/python3.13/site-packages/sipbuild/builder.py", line 280, in _generate_bindings
    buildable = bindings.generate()
                ^^^^^^^^^^^^^^^^^^^
  File "/builddir/build/BUILD/PyQt5-5.15.9/project.py", line 619, in generate
    buildable = super().generate()
                ^^^^^^^^^^^^^^^^^^
  File "/usr/lib64/python3.13/site-packages/sipbuild/bindings.py", line 214, in generate
    output_pyi(spec, project, pyi_path)
  File "/usr/lib64/python3.13/site-packages/sipbuild/generator/outputs/pyi.py", line 53, in output_pyi
    _module(pf, spec, module)
  File "/usr/lib64/python3.13/site-packages/sipbuild/generator/outputs/pyi.py", line 132, in _module
    _class(pf, spec, module, klass, defined)
  File "/usr/lib64/python3.13/site-packages/sipbuild/generator/outputs/pyi.py", line 267, in _class
    _class(pf, spec, module, nested, defined, indent)
  File "/usr/lib64/python3.13/site-packages/sipbuild/generator/outputs/pyi.py", line 289, in _class
    _callable(pf, spec, module, member, klass.overloads,
  File "/usr/lib64/python3.13/site-packages/sipbuild/generator/outputs/pyi.py", line 485, in _callable
    _overload(pf, spec, module, overload, overloaded, first_overload,
  File "/usr/lib64/python3.13/site-packages/sipbuild/generator/outputs/pyi.py", line 575, in _overload
    signature = _python_signature(spec, module, py_signature, defined,
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib64/python3.13/site-packages/sipbuild/generator/outputs/pyi.py", line 599, in _python_signature
    as_str = _argument(spec, module, arg, defined, arg_nr=arg_nr)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib64/python3.13/site-packages/sipbuild/generator/outputs/pyi.py", line 676, in _argument
    s += _type(spec, module, arg, defined, out=out)
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib64/python3.13/site-packages/sipbuild/generator/outputs/pyi.py", line 710, in _type
    return ArgumentFormatter(spec, arg).as_type_hint(module, out, defined)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib64/python3.13/site-packages/sipbuild/generator/outputs/formatters/argument.py", line 327, in as_type_hint
    s += TypeHintManager(self.spec).as_type_hint(hint, out, context,
         ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib64/python3.13/site-packages/sipbuild/generator/outputs/type_hints.py", line 107, in __new__
    manager = cls._spec_manager_map[spec]
              ~~~~~~~~~~~~~~~~~~~~~^^^^^^
  File "/usr/lib64/python3.13/weakref.py", line 415, in __getitem__
    return self.data[ref(key)]
           ~~~~~~~~~^^^^^^^^^^
  File "<string>", line 4, in __eq__
  File "<string>", line 4, in __eq__
  File "<string>", line 4, in __eq__
  [Previous line repeated 495 more times]
RecursionError: maximum recursion depth exceeded in comparison

cc @rhettinger @ericvsmith

Thanks to @swt2c for bisecting the problem. Thanks to @encukou who gave me a hint of what to look for when finding the smaller reproducer.

CPython versions tested on:

3.13

Operating systems tested on:

Linux

Linked PRs

@hroncok hroncok added the type-bug An unexpected behavior, bug, or error label Mar 12, 2024
@erlend-aasland
Copy link
Contributor

cc. also @carljm, who also reviewed 18cfc1e.

@Eclips4 Eclips4 added the stdlib Python modules in the Lib dir label Mar 12, 2024
@terryjreedy
Copy link
Member

I believe lists are documented as checking for identity. In any case, I believe this is the equivalent for lists (in main).

>>> ll = []
>>> ll.append(ll)
>>> ll == ll
True

@hroncok
Copy link
Contributor Author

hroncok commented Mar 12, 2024

I can confirm that reverting 18cfc1e on main makes the comparison return True.

@pochmann3
Copy link

Is relying on that a good idea? Probably still crashes for mutual recursion, just like with lists?

a = []
b = [a]
a.append(b)

a == b

Attempt This Online!

@carljm
Copy link
Member

carljm commented Mar 19, 2024

I merged the attached PR, because it restores the previous behavior for recursive children, and it generally makes sense to short-circuit dataclass equality to True when comparing a dataclass instance to itself.

There could still be backwards-incompatibilities resulting from 18cfc1e. All it would require is a custom user class (not a dataclass) that implements __eq__ by field comparisons without a short-circuit for identity, and allows for recursive children. I'm not sure such a case is likely enough to warrant a pre-emptive revert of 18cfc1e, but if such cases do surface, a revert may be the only way to preserve backward compatibility.

(The other form that could cause a backward incompatibility would be a class that can return False from __eq__ even when comparing an instance to itself, but I think such a class is badly-behaved and we don't need to worry about it.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
stdlib Python modules in the Lib dir type-bug An unexpected behavior, bug, or error
Projects
None yet
Development

No branches or pull requests

6 participants