https://github.com/python/cpython/commit/97181bb336dcf896188ae46d87c909067375e779
commit: 97181bb336dcf896188ae46d87c909067375e779
branch: main
author: Bartosz Sławecki <[email protected]>
committer: ambv <[email protected]>
date: 2026-02-24T17:04:37+01:00
summary:

gh-143535: Dispatch on the second argument if generic method is 
instance-bindable (GH-144615)

Co-authored-by: Jason R. Coombs <[email protected]>

files:
A Misc/NEWS.d/next/Library/2026-02-09-02-16-36.gh-issue-144615.s04x4n.rst
M Doc/whatsnew/3.15.rst
M Lib/functools.py
M Lib/test/test_functools.py

diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst
index cd1ec0e5c452d3..816d45e0756824 100644
--- a/Doc/whatsnew/3.15.rst
+++ b/Doc/whatsnew/3.15.rst
@@ -744,6 +744,10 @@ functools
   callables.
   (Contributed by Serhiy Storchaka in :gh:`140873`.)
 
+* :func:`~functools.singledispatchmethod` now dispatches on the second argument
+  if it wraps a regular method and is called as a class attribute.
+  (Contributed by Bartosz Sławecki in :gh:`143535`.)
+
 
 hashlib
 -------
diff --git a/Lib/functools.py b/Lib/functools.py
index 59fc2a8fbf6219..9bc2ee7e8c894c 100644
--- a/Lib/functools.py
+++ b/Lib/functools.py
@@ -19,7 +19,7 @@
 # import weakref  # Deferred to single_dispatch()
 from operator import itemgetter
 from reprlib import recursive_repr
-from types import GenericAlias, MethodType, MappingProxyType, UnionType
+from types import FunctionType, GenericAlias, MethodType, MappingProxyType, 
UnionType
 from _thread import RLock
 
 
################################################################################
@@ -1060,6 +1060,11 @@ def __init__(self, unbound, obj, cls):
         # Set instance attributes which cannot be handled in __getattr__()
         # because they conflict with type descriptors.
         func = unbound.func
+
+        # Dispatch on the second argument if a generic method turns into
+        # a bound method on instance-level access. See GH-143535.
+        self._dispatch_arg_index = 1 if obj is None and isinstance(func, 
FunctionType) else 0
+
         try:
             self.__module__ = func.__module__
         except AttributeError:
@@ -1088,9 +1093,22 @@ def __call__(self, /, *args, **kwargs):
                                'singledispatchmethod method')
             raise TypeError(f'{funcname} requires at least '
                             '1 positional argument')
-        method = self._dispatch(args[0].__class__)
+        method = self._dispatch(args[self._dispatch_arg_index].__class__)
+
         if hasattr(method, "__get__"):
+            # If the method is a descriptor, it might be necessary
+            # to drop the first argument before calling
+            # as it can be no longer expected after descriptor access.
+            skip_bound_arg = False
+            if isinstance(method, staticmethod):
+                skip_bound_arg = self._dispatch_arg_index == 1
+
             method = method.__get__(self._obj, self._cls)
+            if isinstance(method, MethodType):
+                skip_bound_arg = self._dispatch_arg_index == 1
+
+            if skip_bound_arg:
+                return method(*args[1:], **kwargs)
         return method(*args, **kwargs)
 
     def __getattr__(self, name):
diff --git a/Lib/test/test_functools.py b/Lib/test/test_functools.py
index 3801a82a610891..86652b7fa4d6df 100644
--- a/Lib/test/test_functools.py
+++ b/Lib/test/test_functools.py
@@ -3005,6 +3005,57 @@ def static_func(arg: int) -> str:
         self.assertEqual(A.static_func.__name__, 'static_func')
         self.assertEqual(A().static_func.__name__, 'static_func')
 
+    def test_method_classlevel_calls(self):
+        """Regression test for GH-143535."""
+        class C:
+            @functools.singledispatchmethod
+            def generic(self, x: object):
+                return "generic"
+
+            @generic.register
+            def special1(self, x: int):
+                return "special1"
+
+            @generic.register
+            @classmethod
+            def special2(self, x: float):
+                return "special2"
+
+            @generic.register
+            @staticmethod
+            def special3(x: complex):
+                return "special3"
+
+            def special4(self, x):
+                return "special4"
+
+            class D1:
+                def __get__(self, _, owner):
+                    return lambda inst, x: owner.special4(inst, x)
+
+            generic.register(D1, D1())
+
+            def special5(self, x):
+                return "special5"
+
+            class D2:
+                def __get__(self, inst, owner):
+                    # Different instance bound to the returned method
+                    # doesn't cause it to receive the original instance
+                    # as a separate argument.
+                    # To work around this, wrap the returned bound method
+                    # with `functools.partial`.
+                    return C().special5
+
+            generic.register(D2, D2())
+
+        self.assertEqual(C.generic(C(), "foo"), "generic")
+        self.assertEqual(C.generic(C(), 1), "special1")
+        self.assertEqual(C.generic(C(), 2.0), "special2")
+        self.assertEqual(C.generic(C(), 3j), "special3")
+        self.assertEqual(C.generic(C(), C.D1()), "special4")
+        self.assertEqual(C.generic(C(), C.D2()), "special5")
+
     def test_method_repr(self):
         class Callable:
             def __call__(self, *args):
diff --git 
a/Misc/NEWS.d/next/Library/2026-02-09-02-16-36.gh-issue-144615.s04x4n.rst 
b/Misc/NEWS.d/next/Library/2026-02-09-02-16-36.gh-issue-144615.s04x4n.rst
new file mode 100644
index 00000000000000..1db257ae312e84
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-02-09-02-16-36.gh-issue-144615.s04x4n.rst
@@ -0,0 +1,3 @@
+Methods directly decorated with :deco:`functools.singledispatchmethod` now
+dispatch on the second argument when called after being accessed as class
+attributes. Patch by Bartosz Sławecki.

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to