Singledispatch support for PEP-585 generic types

a code block like:

@dispatch.register
def _(arg: list[int|str]):
    return "this is a list of ints and strings!"

currently returns:

TypeError: Invalid annotation for 'arg'. list[int|str] is not a class.

This means we have to simplify the annotation at the expense of complicating the function, and less support for mypy:

@dispatch.register
def _(arg: list):
    # list[int|str]
    if all(isinstance(i, (int,str)) for i in arg):
        return "this is a list of ints and strings!"

    # elif <other types>:
        # other logic

    else:
        # list[any]

Personally I find this antitheitcal to singledispatch, which is supposed to solve this exact problem.

I didn’t realize I was supposed ask for public opinions BEFORE i write the code, and if not merged I will just make a new package that borrows alot of code from functools.

Im open for “how SHOULD it work?” but not “it’s too hard. how COULD this even work?!”, for the latter see my current implementation: # gh-131996: Add PEP-585 support to singledispatch types by deadPix3l · Pull Request #131995 · python/cpython · GitHub

1 Like

Your implementation is incorrect for types other than list, set, and a few others because it assumes that any type parameter is the type you get when you iterate over something. But that’s not correct in general. For example, iterating over an Awaitable[int] doesn’t give you ints. The implementation also doesn’t look like it would handle dict[A, B] correctly.

These are fundamental problems that can’t be solved at runtime in general. Given a runtime object that is an instance of a generic type, you cannot in general figure out the type parameters that a static type checker would infer.

4 Likes

Fair and valid critiques, thank you!

1 Like

You could consider extending dispatch.register to support accepting a type-is function with which it could recognize the argument. Maybe accept it in the annotation itself, something like the way typer accepts functions in its annotations.

def is_list_int_or_str(x: Any, /) -> TypeIs[list[int | str]]:
  if not isinstance(x, list):
    return False
  return all(isinstance(y, int | str) for y in x)

@dispatch.register
def _(arg: Annotated[list[int | str], dispatch.Recognizer(is_list_int_or_str)]) -> ...:
  ...
1 Like

Back when I started cattrs, almost 10 years ago, it was based solely on singledispatch. But, like folks find out every so often, singledispatch has a lot of limitations, some of which I’ve documented here: Customizing (Un-)structuring - cattrs 24.1.2 documentation.

After a few years I did exactly what you’re suggesting here: in addition to using a singledispatch we use a list of predicate functions for matching. I find this approach much more powerful. I think I may remove singledispatch altogether in a future release and replace it with just issubclass predicates (while keeping the existing predicate dispatch, obviously).

5 Likes