4

I've been struggling to write "varadic" argument lists type definitions.

for example, giving types to:

def foo(fn, *args):
    return fn(*args)

the best I've been able to do is using a suggestion from here:

from typing import overload, Callable, TypeVar

A = TypeVar('A')
B = TypeVar('B')
C = TypeVar('C')
R = TypeVar('R')

@overload
def foo(fn: Callable[[A], R], a: A) -> R: ...
@overload
def foo(fn: Callable[[A, B], R], a: A, b: B) -> R: ...
@overload
def foo(fn: Callable[[A, B, C], R], a: A, b: B, c: C) -> R: ...

def foo(fn, *args):
    return fn(*args)

which mostly does the right thing… for example, given:

def bar(i: int, j: int) -> None:
    print(i)

the following succeeds:

foo(bar, 10, 12)

while these fail:

foo(bar, 10)
foo(bar, 10, 'a')
foo(bar, 10, 12) + 1

but if I check with mypy --strict I get:

test.py:15: error: Function is missing a type annotation

(which is saying that the final foo definition doesn't have any types itself)

I can redefine foo to be:

def foo(fn: Callable[..., R], *args: Any) -> R:
    return fn(*args)

but then when I run mypy --strict I get:

test.py:15: error: Overloaded function implementation does not accept all possible arguments of signature 1
test.py:15: error: Overloaded function implementation does not accept all possible arguments of signature 2
test.py:15: error: Overloaded function implementation does not accept all possible arguments of signature 3

which I don't really understand.

if anyone can suggest a better way of giving types to this sort of function it would be greatly appreciated! if I could also do this without listing lots of overloads that would be nice, the real definitions also have a few "keyword only" arguments that would be nice not to have to repeat each time

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Sam Mason
  • 15,216
  • 1
  • 41
  • 60
  • Perhaps this is related: https://github.com/python/mypy/issues/4619#issuecomment-371368762. I also think if you use your second version of `foo` definition without overloads it will work just fine and will accept all of your combinations. – mehdix Jan 03 '19 at 15:03
  • @MehdiSadeghi thanks for a response, but I don't get it! when you say "without overloads" I presume you mean I should remove the `@overload` decorated definitions? if so, how would it know that `foo(bar, 10, 'a')` was incorrect? – Sam Mason Jan 03 '19 at 16:20

1 Answers1

5

The reason why you're getting the "Overloaded function implementation does not accept all possible arguments..." error is because your overload implementation does not correctly handle calls that look like this: foo(my_callable, a=3, b=4).

After all, according to your overload signatures, the user could in theory explicitly use named arguments for a, b, c, and so forth -- and so, your implementation needs to support those kinds of calls.

There are two different ways you can fix this.

The first way is to tack on a **kwargs: Any and modify your overload implementation to look like this:

def foo(fn: Callable[..., R], *args: Any, **kwargs: Any) -> Any:
    return fn(*args, **kwargs)

Now your implementation will correctly handle these kinds of calls.

The second way is to prefix each of your parameters with two underscores, like so:

@overload
def foo(fn: Callable[[A], R], __a: A) -> R: ...
@overload
def foo(fn: Callable[[A, B], R], __a: A, __b: B) -> R: ...
@overload
def foo(fn: Callable[[A, B, C], R], __a: A, __b: B, __c: C) -> R: ...

def foo(fn: Callable[..., R], *args: Any) -> Any:
    return fn(*args)

When mypy sees a parameter starting with two underscores, it understands that argument is meant to be positional-only. So, mypy will reject calls like foo(my_fn, __a=3, __b=4).

This is a typing-only thing though. Prefixing your parameters with two underscores has no special meaning at runtime.


Regarding your broader question about not having to repeat so many overloads: unfortunately, tacking on a bunch of overloads is the best we can do at the moment. The technique you're using is the same technique typeshed uses to type functions like map(...) and filter(...), for example.

In order to do better, we need a feature called variadic generics -- but they're a complicated feature and mypy unfortunately doesn't support them yet. The plan is to hopefully have them implemented sometime later in 2019 though, so you might be able to rip out the overloads then.

Michael0x2a
  • 58,192
  • 30
  • 175
  • 224
  • thanks for the clear explanation! I tried to find similar examples to see how other people had done it, but didn't find anything, typeshed looks like somewhere I should have looked… I've seen/used similar "multiple overload" tricks to in other languages (e.g. templates in C++) hence using it here, interesting to hear that alternatives are being thought about – Sam Mason Jan 04 '19 at 23:37