overrides: False positive when checking parameter with Callable
Here’s an example:
from typing import Callable
from overrides import override
class X: pass
class Y(X): pass
# X > Y
# Expected: ((Y) -> None) > ((X) -> None)
class A:
def f(self, o: Callable[[X], None]) -> None:
pass
class B(A):
@override
def f(self, o: Callable[[Y], None]) -> None:
pass
Running this, I get:
$ python stuff.py
Traceback (most recent call last):
File "/home/user/tmp/stuff.py", line 13, in <module>
class B(A):
File "/home/user/tmp/stuff.py", line 14, in B
@override
^^^^^^^^
File "/home/user/storage/misc/jupyter/lib/python3.11/site-packages/overrides/overrides.py", line 143, in override
return _overrides(method, check_signature, check_at_runtime)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/user/storage/misc/jupyter/lib/python3.11/site-packages/overrides/overrides.py", line 170, in _overrides
_validate_method(method, super_class, check_signature)
File "/home/user/storage/misc/jupyter/lib/python3.11/site-packages/overrides/overrides.py", line 189, in _validate_method
ensure_signature_is_compatible(super_method, method, is_static)
File "/home/user/storage/misc/jupyter/lib/python3.11/site-packages/overrides/signature.py", line 103, in ensure_signature_is_compatible
ensure_all_kwargs_defined_in_sub(
File "/home/user/storage/misc/jupyter/lib/python3.11/site-packages/overrides/signature.py", line 164, in ensure_all_kwargs_defined_in_sub
raise TypeError(
TypeError: `B.f: o must be a supertype of `typing.Callable[[__main__.X], NoneType]` but is `typing.Callable[[__main__.Y], NoneType]`
But as I understand it, Callable parameters are contravariant, and thus Callable[[Y], None] is a super type of Callable[[X], None].
mypy seems to agree:
$ mypy stuff.py
Success: no issues found in 1 source file
About this issue
- Original URL
- State: open
- Created a year ago
- Comments: 25 (18 by maintainers)
I also just tried with the list example. I can’t get it to work with the built-in
list(mypy says “Variance of TypeVar “T” incompatible with variance in parent type”), but I don’t think the issue is specific toCallableeither. I managed to define a custom class that behaves the same way:Not that a
ContraListwould be practically useful… But mypy is happy with that, and the@overrideis not.You’re correct that function parameter types are contravariant. However, it’s important to understand what this means. When we say function parameter types are contravariant, it means that if
Bis a subtype ofA, then a function that accepts anAcan be replaced with a function that accepts aB(not the other way around). In other words, the function that accepts the more general type is considered the “super type” when comparing functions.So in the case of
feed_any_animal: Callable[[Animal], None]andfeed_only_dogs: Callable[[Dog], None],feed_any_animalis the super type offeed_only_dogs. That’s becausefeed_any_animalcan replacefeed_only_dogsin any code that usesfeed_only_dogswithout causing a type error.To understand why, consider the following scenario:
Let’s say we have a function that calls
feed_only_dogs:You can pass
feed_any_animalto this function without any issues:However, if we have a similar function that calls
feed_any_animal:You can’t pass
feed_only_dogsto this function if you’re planning to feed an animal that’s not aDog:In this sense,
feed_any_animalcan be used anywherefeed_only_dogscan be used, but the reverse is not true. This is whyfeed_any_animalis a super type offeed_only_dogs.To your analogy about
FruitandApple, that example is different from the function example because it deals with return types of methods rather than input parameters. When it comes to return types, it’s true that they are covariant, meaning that a function returning aBcould be replaced with a function returning anAifBis a subtype ofA. That’s the opposite of what happens with function parameter types, which are contravariant. So it’s not accurate to compare a function’s input parameters to a function’s return type when discussing covariance and contravariance.You keep giving me references I already agree with…
Yes, if my example was talking about
lists in the abstract, and their parameter types were invariant, then indeedlist[Food]andlist[DogFood]are incompatible. They would have nothing to do with each other.But that’s not the case. In
feed_animals(self, bucket: list[Food]), I’m using thebucketstrictly to put stuff into it, same way the parameters of aCallableobject can only go in.Those
Callableobject are thefeedfunctions that were used as arguments tofeed_animalsa few examples ago. If we write out the types for thosefeedfunctions in Java, it would look likeWe have the wild cards because Java uses use-site variance, and we have to explicitly write out the contravariant property of function parameter types. Python uses declare-site variance, and that property is built in to
Callable.Notice that we can assign
feed_foodof typeConsumer<? super Food>to a variable declared to beConsumer<? super DogFood>, because the latter is a supertype of the former. If you try to assign the other way, the compiler will error.In general, when we override a method in a subclass, the principle of substitutability must hold. This principle, also known as Liskov Substitution Principle (LSP), is fundamental in object-oriented programming and type theory.
Specifically, in the context of method overriding:
The input parameters (arguments) of the overriding method in the subclass can be the same as or wider (more general) than those in the overridden method of the superclass. This is known as contravariance of method arguments.
The return type of the overriding method in the subclass can be the same as or narrower (more specific) than that in the overridden method of the superclass. This is known as covariance of return types.
In your case, the types in the parameter are inverted. You’re passing
Callable[[X], None]in classAandCallable[[Y], None]in classB, whereYis a subclass ofX. This contradicts the principle of contravariance for method arguments.Therefore, your example doesn’t comply with Liskov’s Substitution Principle, which is likely to cause a TypeError when trying to override the function
fin classB.To correct this, you would want to ensure the principle of contravariance for method arguments. So, class
Ashould takeCallable[[Y], None]and classBshould takeCallable[[X], None], asXis a wider type compared toY. This allows any function that can handleXto be passed in, ensuring substitutability when going fromAtoB.Here is the corrected code:
Please note that the actual functionality of the classes and methods isn’t defined in the code you provided, so my advice is based on type theory and the Liskov Substitution Principle, and not on the specifics of the functionality.