mypy seems to think that (*args, **kwargs) could match to any funtion signature?

Question:

How does mypy apply the Liskov substitution principle to *args, **kwargs parameters?

I thought the following code should fail a mypy check since some calls to f allowed by the Base class are not allowed by C, but it actually passed. Are there any reasons for this?

from abc import ABC, abstractmethod
from typing import Any


class Base(ABC):
    @abstractmethod
    def f(self, *args: Any, **kwargs: Any) -> int:
        pass


class C(Base):
    def f(self, batch: int, train: bool) -> int:
        return 1

I also tried to remove either *args or **kwargs, both failed.

Asked By: ppc

||

Answers:

This has nothing to do with *args or **kwargs per se. The reason for this is strictly the fact that you used typing.Any for both annotations.

The Any annotation is basically a Jedi mind trick for the type checker to the effect of:

These are the types you were looking for.

No matter what, it will always pass.

For this reason, the typing documentation specifically recommends to use object as much as possible instead of Any, when you mean to say something like "the broadest possible type". Any should be reserved as the last resort, when you bump against the limits of the Python typing system.

The mypy docs also have a section explaining the difference between Any and object.

If you change even one of those Any annotations to object, you will be rightfully chastised by mypy with an [override] error for C.f.

Example:

from typing import Any

class Base:
    def f(self, *args: object, **kwargs: Any) -> int:
        return 2

class C(Base):
    def f(self, batch: int, train: bool) -> int:  # now this is an error
        return 1

Whereas the combination of saying "any number of positional and keyword-arguments" together with "each argument will always pass the type check" essentially translates to "no override will ever be wrong" (in terms of arguments).

So I would suggest using object instead of Any everywhere, unless you cannot avoid using the latter.

These confusions are one of the reasons I think the choice to name this construct Any is so unfortunate.

PS

My first paragraph was not well worded. As @SUTerliakov explained more clearly, the reason this override does not cause an error is specifically because of the combination of the *args/**kwargs parameters and them being annotated with Any. Only if both conditions are met, does mypy make this exception.

Answered By: Daniil Fajnberg

Unlike Daniil said in currently accepted answer, the reason is exactly (*args: Any, **kwargs: Any) signature part.

Please check the corresponding discussion on mypy issue tracker:

I actually like this idea, I have seen this confusion several times, and although it is a bit unsafe, most of the time when people write (*args, **kwargs) it means "don’t care", rather than "should work for all calls".

[GVR] Agreed, this is a case where practicality beats purity.

So, mypy gives a special treatment to functions of form

# _T is arbitrary type
class _:
    def _(self, *args, **kwargs) -> _T: ...

and considers them fully equivalent to Callable[..., _T].

Yes, this actually violates LSP, of course, but this was designed specially to allow declaring functions with signature "just ignore my parameters".

To declare the broadest possible function that really accepts arbitrary positional and keyword arguments, you should use object in signature instead.

Answered By: SUTerliakov