diff --git a/Doc/deprecations/pending-removal-in-3.18.rst b/Doc/deprecations/pending-removal-in-3.18.rst index 19113aab981bbc6..a9dee56fe405a97 100644 --- a/Doc/deprecations/pending-removal-in-3.18.rst +++ b/Doc/deprecations/pending-removal-in-3.18.rst @@ -16,3 +16,12 @@ Pending removal in Python 3.18 * ``import`` lines in :file:`{name}.pth` files are silently ignored. (Contributed by Barry Warsaw in :gh:`148641`.) + +* :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.18. Annotate the dispatch + parameter or pass the dispatch type explicitly instead. + (Contributed by Bartosz Sławecki in :gh:`143465`.) diff --git a/Doc/library/functools.rst b/Doc/library/functools.rst index 2b46978f058102b..f572abb685f26c5 100644 --- a/Doc/library/functools.rst +++ b/Doc/library/functools.rst @@ -506,9 +506,9 @@ 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. For + functions annotated with types, the decorator can infer the dispatch type + automatically:: >>> @fun.register ... def _(arg: int, verbose=False): @@ -540,8 +540,9 @@ 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:: + For code which doesn't use type annotations, or where the first resolved + annotation does not belong to the dispatch argument, the appropriate type + can be passed explicitly to the decorator itself:: >>> @fun.register(complex) ... def _(arg, verbose=False): @@ -662,6 +663,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.18. Annotate the dispatch + parameter or pass the dispatch type explicitly to :func:`!register` + instead. + .. class:: singledispatchmethod(func) @@ -686,6 +693,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/Lib/functools.py b/Lib/functools.py index 8425f6030010f3d..da78c76ee69b9da 100644 --- a/Lib/functools.py +++ b/Lib/functools.py @@ -990,6 +990,17 @@ def register(cls, func=None): f"Invalid annotation for {argname!r}. " f"{cls!r} is not a class." ) + if argname == 'return': + import os + import warnings + warnings.warn( + "Using the return annotation to infer the dispatch type " + "is deprecated and will raise TypeError in Python 3.18. " + "Annotate the dispatch parameter or pass the dispatch " + "type explicitly to register().", + DeprecationWarning, + skip_file_prefixes=(os.path.dirname(__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..8cb71dc7d76c2e9 100644 --- a/Lib/test/test_functools.py +++ b/Lib/test/test_functools.py @@ -3337,6 +3337,38 @@ 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" + register = compile( + "dispatcher.register(impl)", "", "exec" + ) + + @functools.singledispatch + def func(arg): + return "base" + + def impl(arg) -> int: + return "int" + + with self.assertWarnsRegex(DeprecationWarning, msg) as cm: + exec(register, {'dispatcher': func, 'impl': impl}) + self.assertEqual(cm.filename, "") + 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: + exec(register, {'dispatcher': C.method, 'impl': method_impl}) + self.assertEqual(cm.filename, "") + 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..519d7496bb139bc --- /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.18. Annotate the dispatch parameter or pass the dispatch type +explicitly instead. Patch by Bartosz Sławecki.