Reputation: 73
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.
Upvotes: 7
Views: 5315
Reputation: 18683
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.
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.
Upvotes: 1
Reputation: 7943
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.
Upvotes: 5