Skip to content

functools.singledispatch does not support Union types #90172

Closed

Description

@evansd
mannequin
BPO 46014
Nosy @rhettinger, @ambv, @uriyyo, @AlexWaygood, @evansd
PRs
  • bpo-46014: Add ability to use typing.Union with singledispatch #30017
  • bpo-46014: Add docs regarding functools.singledispatch changes #32282
  • Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.

    Show more details

    GitHub fields:

    assignee = None
    closed_at = <Date 2021-12-10.23:29:02.779>
    created_at = <Date 2021-12-08.13:34:12.625>
    labels = ['type-feature', 'library', '3.9', '3.10', '3.11']
    title = 'functools.singledispatch does not support Union types'
    updated_at = <Date 2022-04-03.08:22:50.956>
    user = 'https://github.com/evansd'

    bugs.python.org fields:

    activity = <Date 2022-04-03.08:22:50.956>
    actor = 'uriyyo'
    assignee = 'none'
    closed = True
    closed_date = <Date 2021-12-10.23:29:02.779>
    closer = 'lukasz.langa'
    components = ['Library (Lib)']
    creation = <Date 2021-12-08.13:34:12.625>
    creator = 'evansd'
    dependencies = []
    files = []
    hgrepos = []
    issue_num = 46014
    keywords = ['patch']
    message_count = 6.0
    messages = ['408018', '408261', '408262', '408272', '409366', '409369']
    nosy_count = 6.0
    nosy_names = ['rhettinger', 'wrobell', 'lukasz.langa', 'uriyyo', 'AlexWaygood', 'evansd']
    pr_nums = ['30017', '32282']
    priority = 'normal'
    resolution = 'fixed'
    stage = 'resolved'
    status = 'closed'
    superseder = None
    type = 'enhancement'
    url = 'https://bugs.python.org/issue46014'
    versions = ['Python 3.9', 'Python 3.10', 'Python 3.11']

    Activity

    1. evansd commented on Dec 8, 2021

      evansdmannequin
      MannequinAuthor

      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

    2. added
      3.11only security fixes
      stdlibStandard Library Python modules in the Lib/ directory
      type-featureA feature request or enhancement
      on Dec 8, 2021
    3. 3 remaining items

    4. ambv commented on Dec 10, 2021

      @ambv
      Contributor

      New changeset 3cb357a by Yurii Karabas in branch 'main':
      bpo-46014: Add ability to use typing.Union with singledispatch (GH-30017)
      3cb357a

    5. ambv commented on Dec 10, 2021

      @ambv
      Contributor

      Support added as an enhancement in Python 3.11. Thanks, Yurii! ✨ 🍰 ✨

    6. AlexWaygood commented on Dec 10, 2021

      @AlexWaygood
      Member

      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.)

    7. wrobell commented on Dec 30, 2021

      wrobellmannequin
      Mannequin

      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].

    8. AlexWaygood commented on Dec 30, 2021

      @AlexWaygood
      Member
      >>> 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).

    9. transferred this issue fromon Apr 10, 2022
    10. iritkatriel commented on Apr 11, 2022

      @iritkatriel
      Member

      Reopening as per #32282 (comment)

    11. JelleZijlstra commented on Apr 16, 2022

      @JelleZijlstra
      Member
      >>> 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.

      It does indeed work (continuing from the OP's example):

      >>> @test.register
      ... def _(arg: str  | None):
      ...     print(f"{arg} may be a string")
      ... 
      >>> test(None)
      None may be a string
      >>> test("x")
      x may be a string

      But it's not obvious that it should, because None isn't a type. It apparently works because None gets turned into type(None) somewhere along the way in typing.py, but this seems worth testing explicitly in test_functools.

      So anyone up for adding a test case that uses an Optional type?

    12. added a commit that references this issue on May 3, 2022
    13. JelleZijlstra commented on May 3, 2022

      @JelleZijlstra
      Member

      Thanks @Thaddeus1499 for contributing more tests.

    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

    Metadata

    Metadata

    Assignees

    No one assigned

      Labels

      3.10 (EOL)end of life3.11only security fixes3.9 (EOL)end of lifestdlibStandard Library Python modules in the Lib/ directorytype-featureA feature request or enhancement

      Projects

      No projects

        Milestone

        No milestone

        Relationships

        None yet

        Development

        No branches or pull requests

        Issue actions