Reputation: 141
My project depends on another project that stores the type annotations in stub files. Within the .py file the other project defines a base class that I need to inherit from a follows
# within a .py file
class Foo:
def bar(self, *baz):
raise NotImplementedError
In the corresponding .pyi stub they annotate it as follows:
# whitin a .pyi file
from typing import Generic, TypeVar, Callable
T_co = TypeVar("T_co", covariant=True)
class Foo(Generic[T_co]):
bar: Callable[..., T_co]
For my project I want to do the type annotations inline, i.e. in the .py file, and tried to do it in a subclass of Foo
as follows:
# within a .py file
class SubFoo(Foo):
def bar(self, baz: float) -> str:
pass
Running mypy
on this results in the following error
error: Signature of "bar" incompatible with supertype "Foo"
If I remove my inline annotations and add this to a .pyi stub
# within a .pyi file
class SubFoo(Foo):
bar: Callable[[float], str]
mypy
runs fine.
I thought that both methods are equivalent, but apparently that is not the case. Can someone explain to me how they differ and what I need to change to make this work with inline annotations?
It became clear in the comments of @Michael0x2a answer that the error is only reproducible if you indeed use a .py and a .pyi file. You can download the examples from above here.
Upvotes: 3
Views: 2275
Reputation: 64228
As a caveat, it's unclear to me exactly what your code looks like. You have several different versions of Foo
defined and I'm not sure exactly which one you're trying to subclass -- your question is missing a minimum reproducible example.
But I'm guessing you're trying to do something like this?
class Foo:
def bar(self, *baz: float) -> str:
raise NotImplementedError
class SubFoo(Foo):
def bar(self, baz: float) -> str:
pass
If so, the problem is that according to the signature of the base class, it'd be legal to do something like this, since Foo.bar(...)
is defined to accept a variable number of arguments.
f = Foo()
f.bar(1, 2, 3, 4, 5, 6, 7, 8)
But if we try using your subclass in place of Foo, this code would fail since it only accepts one argument.
This idea that a subclass should always be capable of taking the place of the parent class without causing type errors and without violating the existing preconditions and postconditions of your code is known as the Liskov substitution principle.
But in that case, why does doing the following type check?
class Foo:
bar: Callable[..., str]
class SubFoo(Foo):
def bar(self, baz: float) -> str:
pass
This is because since the parent type's signature is Callable[..., str]
, mypy actually ends up skipping checking the function arguments altogether. The ...
is basically saying "please don't bother type checking anything related to my arguments".
It's sort of similar to how using the Any
type lets you mix dynamic types with static ones. Similarly, Callable[..., str]
lets you express callables with dynamic/undetermined signatures.
Contrast this with the following program:
class Foo:
def bar(self, *args: Any, **kwargs: Any) -> str:
pass
class SubFoo(Foo):
def bar(self, baz: float) -> str:
pass
Unlike the previous program, this one does not type check -- while Foo.bar
still can accept any arguments, the "structure" of the arguments in this case is not left flexible and mypy will now insist that your subclass must also be capable of accepting an arbitrary number of arguments.
As a final note, it's important to note that none of this behavior has anything to do with whether your type hints are defined in a stub or not. Rather, it all comes down to what the actual types of your functions are.
Upvotes: 2