classification
Title: functools.singledispatch does not support Union types
Type: enhancement Stage: resolved
Components: Library (Lib) Versions: Python 3.11, Python 3.10, Python 3.9
process
Status: closed Resolution: fixed
Dependencies: Superseder:
Assigned To: Nosy List: AlexWaygood, evansd, lukasz.langa, rhettinger, uriyyo, wrobell
Priority: normal Keywords: patch

Created on 2021-12-08 13:34 by evansd, last changed 2021-12-30 13:42 by AlexWaygood. This issue is now closed.

Pull Requests
URL Status Linked Edit
PR 30017 merged uriyyo, 2021-12-10 01:16
Messages (6)
msg408018 - (view) Author: Dave Evans (evansd) Date: 2021-12-08 13:34
It's not currently possible to use `singledispatch` with a function annotated with a Union type, although the desired behaviour is clear.

Example:
```
    from functools import singledispatch
    from typing import Union

    @singledispatch
    def test(arg):
        raise ValueError(arg)

    @test.register
    def _(arg: Union[int, float]):
        print(f"{arg} is a number")

    test(1)
```

This dies with:

    TypeError: Invalid annotation for 'arg'. typing.Union[int, float] is not a class.

I've implemented a workaround which digs the types out of the union and registers them one-by-one:
```
    from functools import singledispatch
    from typing import Union, get_type_hints

    def register_for_union_type(target_fn):
        def decorator(fn):
            arg_type = list(get_type_hints(fn).values())[0]
            assert arg_type.__origin__ is Union
            for type_ in arg_type.__args__:
                fn = target_fn.register(type_)(fn)
            return fn

        return decorator

    @singledispatch
    def test(arg):
        raise ValueError(arg)

    @register_for_union_type(test)
    def _(arg: Union[int, float]):
        print(f"{arg} is a number")

    test(1)

```

Looking at the source for `singledispatch` is doesn't look like it would *too* difficult to add support for something like this, though of course there may be subtleties I've missed. Would you be open to a patch?

The only other mentions I've found of this online are:
https://mail.python.org/archives/list/python-ideas@python.org/thread/HF5HDXQ2SXZHO3TWODIRQATTE4TCAWPL/
https://stackoverflow.com/questions/61721761/type-hints-and-singledispatch-how-do-i-include-union-in-an-extensible-w
msg408261 - (view) Author: Łukasz Langa (lukasz.langa) * (Python committer) Date: 2021-12-10 23:28
New changeset 3cb357a2e6ac18ee98db5d450414e773744e3c76 by Yurii Karabas in branch 'main':
bpo-46014: Add ability to use typing.Union with singledispatch (GH-30017)
https://github.com/python/cpython/commit/3cb357a2e6ac18ee98db5d450414e773744e3c76
msg408262 - (view) Author: Łukasz Langa (lukasz.langa) * (Python committer) Date: 2021-12-10 23:29
Support added as an enhancement in Python 3.11. Thanks, Yurii! ✨ 🍰 ✨
msg408272 - (view) Author: Alex Waygood (AlexWaygood) * (Python triager) Date: 2021-12-10 23:53
This is awesome! Should a note be added to the functools documentation mentioning the new feature? (Genuine question — I'm not sure whether it's necessary myself.)
msg409366 - (view) Author: wrobell (wrobell) Date: 2021-12-30 12:55
Will it support Optional as well?

According to PEP-604, these two shall be equivalent (using Python 3.10 below)?
```
type(int | None)
types.UnionType

type(tp.Optional[int])
typing._UnionGenericAlias
```

Also, IMHO, the documentation of singledispatch should be improved. It needs to state that only Python types are supported with few exceptions (union type now), but other annotation types are not supported, i.e. generics like `list[int]`.
msg409369 - (view) Author: Alex Waygood (AlexWaygood) * (Python triager) Date: 2021-12-30 13:42
```
>>> from typing import Optional, get_origin
>>> get_origin(Optional[int])
typing.Union
```

^Because of that, it will work with typing.Optional as well as typing.Union and types.UnionType, yes.

I am planning on submitting a docs PR at some point in the next few days (probably linked to a new BPO issue).
History
Date User Action Args
2021-12-30 13:42:12AlexWaygoodsetmessages: + msg409369
2021-12-30 12:55:43wrobellsetnosy: + wrobell
messages: + msg409366
2021-12-10 23:53:30AlexWaygoodsetmessages: + msg408272
2021-12-10 23:29:02lukasz.langasetstatus: open -> closed
resolution: fixed
messages: + msg408262

stage: patch review -> resolved
2021-12-10 23:28:07lukasz.langasetmessages: + msg408261
2021-12-10 01:16:22uriyyosetkeywords: + patch
nosy: + uriyyo

pull_requests: + pull_request28241
stage: patch review
2021-12-08 13:47:08AlexWaygoodsetnosy: + rhettinger, lukasz.langa, AlexWaygood

versions: - Python 3.7, Python 3.8
2021-12-08 13:34:12evansdcreate