Skip to content

Regression in Django with singledispatchmethod on models #127750

Closed

Description

@vodik

Bug report

Bug description:

Also reported upstream to Django here.

The following code works on Python 3.12 and I believe #85160 is the cause.

When traversing a relationship and then calling a singledispatchmethod method registered on a model, the reference to self seems to get cached and locked to the first created model.

class Test(models.Model):
    ...

class Test2(models.Model):
    relationship = models.ForeignKey("Test", on_delete=models.CASCADE)

    @singledispatchmethod
    def bug(self, x):
        print(">", id(self))

    @bug.register
    def _(self, x: int):
        print(">", id(self))

and then it's pretty easy to trigger:

for t in test.test2_set.all():
    print("id:", id(t))  # always changes
    t.bug(4)             # only ever prints a single id

CPython versions tested on:

3.13

Operating systems tested on:

macOS

Linked PRs

Activity

  1. added
    type-bugAn unexpected behavior, bug, or error
    on Dec 8, 2024
  2. added
    stdlibStandard Library Python modules in the Lib/ directory
    3.12only security fixes
    3.13only security fixes
    3.14bugs and security fixes
    and removed
    3.12only security fixes
    on Dec 8, 2024
  3. vodik commented on Dec 9, 2024

    @vodik
    Author

    I see what the issue is now: https://github.com/AlexWaygood/cpython/blob/3d8d4ba5f5367f2d87488b00afd55f75401c2b7d/Lib/functools.py#L950

    Since dictionaries use __hash__/__eq__ for determining if a key is present, we can have collisions. Django hashs models based on the primary key. Here's a way to reproduce the same bug with just the stdlib:

    from functools import singledispatchmethod
    from dataclasses import dataclass
    
    @dataclass(frozen=True)  # or (unsafe_hash=True)
    class Test:
        pk: int
    
        @singledispatchmethod
        def broken(self, x):
            raise NotImplemenetedError
    
        @broken.register
        def _(self, x: int):
            return id(self)
    
    
    t1 = Test(1)
    print(id(t1))
    
    t2 = Test(1)
    print(id(t2))
    
    assert id(t1) == t1.broken(2)  # both t1 and t2 will share the same dispatcher
    assert id(t2) == t2.broken(2)  # fails! 
  4. vodik commented on Dec 9, 2024

    @vodik
    Author

    I have a fix, I think. Working on a PR.

  5. added 3 commits that reference this issue on Dec 9, 2024
  6. vodik commented on Dec 9, 2024

    @vodik
    Author

    I'm pretty sure the code in question is also leaking memory. See comment here.

  7. eendebakpt commented on Dec 9, 2024

    @eendebakpt
    Contributor

    @vodik There is indeed a behavior change between 3.12 and 3.13, so perhaps that needs to be fixed. Another example showing the issue is:

    from functools import lru_cache
    from dataclasses import dataclass
    
    @dataclass(frozen=True)  # or (unsafe_hash=True)
    class Test:
        pk: int
    
    t1 = Test(1)
    t2 = Test(1)  
    
    @lru_cache
    def return_id(obj):
        return id(obj)
    
    print(return_id(t1))
    print(return_id(t2))
  8. 8 remaining items

  9. added 4 commits that reference this issue on Feb 20, 2025
  10. added a commit that references this issue on Feb 20, 2025
  11. added a commit that references this issue on Feb 20, 2025
  12. added a commit that references this issue on Mar 5, 2025
  13. added a commit that references this issue on Jan 11, 2026
  14. added 2 commits that reference this issue on Jan 11, 2026
  15. added 2 commits that reference this issue on Jan 11, 2026
  16. added a commit that references this issue on Jan 12, 2026
  17. added a commit that references this issue on Feb 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    3.13only security fixes3.14bugs and 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