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 2b46978f058102b..2bd9588cc64488a 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 c06e6930ac78bff..d685b1d88c78047 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -706,6 +706,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`: * Soft-deprecated since Python 3.15, using ``'F'`` and ``'D'`` type codes are now 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 941dd7249a48d91..3cfa07e0adca9b7 100644 --- a/Lib/test/test_functools.py +++ b/Lib/test/test_functools.py @@ -3337,6 +3337,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.