https://github.com/python/cpython/commit/641ef0aea9831669ba8e9b2151e3ddf956a4aa00
commit: 641ef0aea9831669ba8e9b2151e3ddf956a4aa00
branch: main
author: Bartosz Sławecki <[email protected]>
committer: encukou <[email protected]>
date: 2026-10-21T18:04:48+02:00
summary:

gh-84644: Deprecate singledispatch on return annotation (GH-143465)


Co-authored-by: Jelle Zijlstra <[email protected]>
Co-authored-by: Jason R. Coombs <[email protected]>
Co-authored-by: Carey Metcalfe <[email protected]>
Co-authored-by: Bénédikt Tran <[email protected]>

files:
A Misc/NEWS.d/next/Library/2026-01-06-09-13-53.gh-issue-84644.V_cYP3.rst
M Doc/deprecations/pending-removal-in-3.21.rst
M Doc/library/functools.rst
M Doc/whatsnew/3.16.rst
M Lib/functools.py
M Lib/test/test_functools.py

diff --git a/Doc/deprecations/pending-removal-in-3.21.rst 
b/Doc/deprecations/pending-removal-in-3.21.rst
index c2546b7fc451aed..9fd0cd15aac5476 100644
--- a/Doc/deprecations/pending-removal-in-3.21.rst
+++ b/Doc/deprecations/pending-removal-in-3.21.rst
@@ -18,6 +18,15 @@ Pending removal in Python 3.21
   * The ``dims`` property of ``ast.Tuple`` will be removed in Python 3.21. Use
     the ``ast.Tuple.elts`` property instead.
 
+* :mod:`functools`:
+
+  * Using a return annotation to infer the dispatch type for
+    :func:`functools.singledispatch` or
+    :class:`functools.singledispatchmethod` is deprecated since Python 3.16
+    and will raise :exc:`TypeError` in Python 3.21. Annotate the dispatch
+    parameter or pass the dispatch type explicitly instead.
+    (Contributed by Bartosz Sławecki in :gh:`143465`.)
+
 * :mod:`struct`:
 
   * Soft-deprecated since Python 3.15, using ``'F'`` and ``'D'`` type codes 
are now
diff --git a/Doc/library/functools.rst b/Doc/library/functools.rst
index debb7a7c3c2cc0e..4eeafad3fbd9156 100644
--- a/Doc/library/functools.rst
+++ b/Doc/library/functools.rst
@@ -506,24 +506,37 @@ The :mod:`!functools` module defines the following 
functions:
       :no-typesetting:
 
    To add overloaded implementations to the function, use the :func:`!register`
-   attribute of the generic function, which can be used as a decorator.  For
-   functions annotated with types, the decorator will infer the type of the
-   first argument automatically::
+   attribute of the generic function, which can be used as a decorator.
+   Pass the dispatch type to the decorator::
 
-     >>> @fun.register
-     ... def _(arg: int, verbose=False):
+     >>> @fun.register(int)
+     ... def _(arg, verbose=False):
      ...     if verbose:
      ...         print("Strength in numbers, eh?", end=" ")
      ...     print(arg)
      ...
-     >>> @fun.register
-     ... def _(arg: list, verbose=False):
+     >>> @fun.register(list)
+     ... def _(arg, verbose=False):
      ...     if verbose:
      ...         print("Enumerate this:")
      ...     for i, elem in enumerate(arg):
      ...         print(i, elem)
 
-   :class:`typing.Union` can also be used::
+   As a shortcut, for functions annotated with types, the decorator can infer
+   the dispatch type without an explicit type argument::
+
+     >>> @fun.register
+     ... def _(arg: int, verbose=False):
+     ...     if verbose:
+     ...         print("Strength in numbers, eh?", end=" ")
+     ...     print(arg)
+     ...
+
+   This shortcut uses the first annotation, even if it belongs to another
+   parameter or the return value. Annotate the dispatch parameter, or pass the
+   dispatch type explicitly if its annotation is not first.
+
+   :class:`typing.Union` can also be used with this shortcut::
 
     >>> @fun.register
     ... def _(arg: int | float, verbose=False):
@@ -540,16 +553,6 @@ The :mod:`!functools` module defines the following 
functions:
     ...         print(i, elem)
     ...
 
-   For code which doesn't use type annotations, the appropriate type
-   argument can be passed explicitly to the decorator itself::
-
-     >>> @fun.register(complex)
-     ... def _(arg, verbose=False):
-     ...     if verbose:
-     ...         print("Better than complicated.", end=" ")
-     ...     print(arg.real, arg.imag)
-     ...
-
    For code that dispatches on a collections type (e.g., ``list``), but wants
    to typehint the items of the collection (e.g., ``list[int]``), the
    dispatch type should be passed explicitly to the decorator itself with the
@@ -662,6 +665,12 @@ The :mod:`!functools` module defines the following 
functions:
       The :func:`~singledispatch.register` attribute now supports
       :class:`typing.Union` as a type annotation.
 
+   .. deprecated:: 3.16
+      Using a return annotation to infer the dispatch type is deprecated and
+      will raise :exc:`TypeError` in Python 3.21. Annotate the dispatch
+      parameter or pass the dispatch type explicitly to :func:`!register`
+      instead.
+
 
 .. class:: singledispatchmethod(func)
 
@@ -686,6 +695,20 @@ The :mod:`!functools` module defines the following 
functions:
         def _(self, arg: bool):
             return not arg
 
+   When *self* or *cls* is annotated, pass the dispatch type explicitly rather
+   than relying on annotation inference::
+
+    from typing import Self
+
+    class Negator:
+        @singledispatchmethod
+        def neg(self, arg):
+            raise NotImplementedError("Cannot negate a")
+
+        @neg.register(int)
+        def _(self: Self, arg: int):
+            return -arg
+
    ``@singledispatchmethod`` supports nesting with other decorators such as
    :deco:`classmethod`. Note that to allow for
    ``dispatcher.register``, ``singledispatchmethod`` must be the *outer most*
diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst
index c2177d0ca3e286e..c7ba9235a1a3267 100644
--- a/Doc/whatsnew/3.16.rst
+++ b/Doc/whatsnew/3.16.rst
@@ -1079,6 +1079,16 @@ New deprecations
     3.9, now issues a deprecation warning on use. This property is slated for
     removal in 3.21. Use ``ast.Tuple.elts`` instead.
 
+* :mod:`functools`:
+
+  * Using a return annotation to infer the dispatch type for
+    :func:`functools.singledispatch` or
+    :class:`functools.singledispatchmethod` now emits a
+    :exc:`DeprecationWarning` and will raise :exc:`TypeError` in Python 3.21.
+    Annotate the dispatch parameter or pass the dispatch type explicitly
+    instead.
+    (Contributed by Bartosz Sławecki in :gh:`143465`.)
+
 * :mod:`struct`:
 
   * The ``'F'`` and ``'D'`` type codes, soft-deprecated since Python 3.15,
diff --git a/Lib/functools.py b/Lib/functools.py
index 8425f6030010f3d..2c08791d4d2651b 100644
--- a/Lib/functools.py
+++ b/Lib/functools.py
@@ -990,6 +990,16 @@ def register(cls, func=None):
                         f"Invalid annotation for {argname!r}. "
                         f"{cls!r} is not a class."
                     )
+            if argname == 'return':
+                import warnings
+                warnings.warn(
+                    "Using the return annotation to infer the dispatch type "
+                    "is deprecated and will raise TypeError in Python 3.21. "
+                    "Annotate the dispatch parameter or pass the dispatch "
+                    "type explicitly to register() decorator.",
+                    DeprecationWarning,
+                    skip_file_prefixes=(__file__,),
+                )
 
         if isinstance(cls, UnionType):
             for arg in cls.__args__:
diff --git a/Lib/test/test_functools.py b/Lib/test/test_functools.py
index b46a1ce6d634a5f..6aa7916adc0bb86 100644
--- a/Lib/test/test_functools.py
+++ b/Lib/test/test_functools.py
@@ -3371,6 +3371,34 @@ def _(arg: typing.Union[int, typing.Iterable[str]]):
             'int | typing.Iterable[str] not all arguments are classes.'
         )
 
+    def test_return_annotation_deprecated(self):
+        msg = (
+            "Using the return annotation to infer the dispatch type "
+            r"is deprecated and will raise TypeError in Python 3\.21\."
+        )
+
+        @functools.singledispatch
+        def func(arg):
+            return "base"
+        def impl(arg) -> int:
+            return "int"
+        with self.assertWarnsRegex(DeprecationWarning, msg) as cm:
+            func.register(impl)
+        self.assertEqual(cm.filename, __file__)
+        self.assertIs(func.registry[int], impl)
+
+        class C:
+            @functools.singledispatchmethod
+            def method(self, arg):
+                return "base"
+        def method_impl(self) -> str:
+            return "str"
+        with self.assertWarnsRegex(DeprecationWarning, msg) as cm:
+            C.method.register(method_impl)
+        self.assertEqual(cm.filename, __file__)
+        dispatcher = C.__dict__['method'].dispatcher
+        self.assertIs(dispatcher.registry[str], method_impl)
+
     def test_invalid_positional_argument(self):
         @functools.singledispatch
         def f(*args, **kwargs):
diff --git 
a/Misc/NEWS.d/next/Library/2026-01-06-09-13-53.gh-issue-84644.V_cYP3.rst 
b/Misc/NEWS.d/next/Library/2026-01-06-09-13-53.gh-issue-84644.V_cYP3.rst
new file mode 100644
index 000000000000000..a1f4a598c0e0701
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-01-06-09-13-53.gh-issue-84644.V_cYP3.rst
@@ -0,0 +1,5 @@
+Deprecate using a return annotation to infer the dispatch type for
+:func:`functools.singledispatch` and
+:class:`functools.singledispatchmethod`. It will raise :exc:`TypeError` in
+Python 3.21. Annotate the dispatch parameter or pass the dispatch type
+explicitly instead. 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