Although the method signature in Sub
is compatible with Super
, mypy rejects the override: Signature of "method" incompatible with supertype "Super"
.
I'm using
First, I made following test.pyi
.
from typing import overload
class Super:
def method(self, arg:Other|Super)->Super: pass
class Sub(Super):
@overload
def method(self, arg:Other|Sub)->Sub: pass
@overload
def method(self, arg:Super)->Super: pass
class Other: pass
Then, when I ran mypy test.pyi
in command line, mypy produced the following diagnostic:
test.pyi:7: error: Signature of "method" incompatible with supertype "Super" [override]
test.pyi:7: note: Superclass:
test.pyi:7: note: def method(self, arg: Other | Super) -> Super
test.pyi:7: note: Subclass:
test.pyi:7: note: @overload
test.pyi:7: note: def method(self, arg: Other | Sub) -> Sub
test.pyi:7: note: @overload
test.pyi:7: note: def method(self, arg: Super) -> Super
Found 1 error in 1 file (checked 1 source file)
I checked type of both Super.method
and Sub.method
's I/O, and found that there's no pattern that violates LSP (Liskov Substitution Principle).
Overloaded Sub.method
can
arg
of type Other|Super
(= Other|Sub
+ Super
) andSuper
(= Sub
+ Super
).Above input and output type matches the signature of Super.method
.
So, I have no idea to be Signature of "method" incompatible with supertype "Super"
.
Following is I/O table of method
.
I\O | Super.method |
Sub.method |
Compared to Super , Sub 's return is: |
Adhering to LSP* |
---|---|---|---|---|
Other |
Super |
Sub |
narrower | Yes |
Sub |
Super |
Sub |
narrower | Yes |
Super |
Super |
Super |
the same | Yes |
Other|Sub |
Super |
Sub |
narrower | Yes |
Other|Super |
Super |
Super ** |
the same | Yes |
*The LSP requires that the return type of a sub method be narrower or equal to the return type of the super method.
**Sub.method
returns
Sub
when Other
input andSuper
when Super
input,so it returns Sub|Super
when Other|Super
input.
Sub|Super
means Super
.
As you can see from the table above, there are no patterns that violate LSP.
So, I think that mypy error message Signature of "method" incompatible with supertype "Super"
is incorrect.
Is my code wrong?
Also, if my code is not wrong and mypy's error message is wrong, where can I ask?
Although it's not a complete solution, I found a simple way to hide the error.
from typing import overload
class Super:
def method(self, arg:Other|Super)->Super: pass
class Sub(Super):
@overload
def method(self, arg:Other|Sub)->Sub: pass
@overload
def method(self, arg:Super)->Super: pass
# Start of hiding error
@overload
def method( # type: ignore[overload-cannot-match]
self, arg:Other|Super)->Super: pass
# End of hiding error
class Other: pass
As a last overload, I added a Sub.method
with the exact same signature as Super.method
.
However, when this issue is resolved in a future version, it will mean that the code I added will not be reached and we should get a [overload-cannot-match]
error. Therefore, I added # type: ignore[overload-cannot-match]
to ignore this error in advance.
(At first glance, it may seem like I am simply silencing errors with type: ignore
, but this is not relevant to mypy as of now. This is merely a deterrent against future error.)
As pointed out in a pyright ticket about this, the typing spec mentions this behaviour explicitly:
If a callable
B
is overloaded with two or more signatures, it is assignable to callableA
if at least one of the overloaded signatures inB
is assignable toA
There's a mypy ticket asking about the same problem.
However, your code is in fact safe, and the table in your post proves that. That's just a limitation of the specification and/or typecheckers.
As discussed in comments, it may seem like the problem is the union type itself (Other | Super
can't be dispatched to either of overloads), but it isn't true: mypy uses union math in that case, and overloaded call return type is a union of return types of matched overloads if all union members can be dispatched to one of them. Here's the source where this magic happens, read the comments there if you're interested - the main checker path is documented well.
Now, given that your code doesn't typecheck only due to a typechecker/spec issue, you have several options:
# type: ignore[override]
- why not? Your override is safe, just tell mypy to STFU.
Add a signature to match the spec requirements as in your last paragraph. But please don't add an unused ignore comment - there's a --warn-unused-ignores
flag for mypy
which is really useful. Don't add such ignore, just add a free-text comment explaining the problem and linking here or to the mypy issue.
Just extend the second signature. That's safe - overloads are tried in order, the first match wins (well, not exactly, but in simple cases without *args/**kwargs and ParamSpec that's true):
class Sub(Super):
@overload
def method(self, arg: Other | Sub) -> Sub: ...
@overload
def method(self, arg: Other | Super) -> Super: ...
But I'd just go with an ignore comment and explain the problem:
class Sub(Super):
# The override is safe, but doesn't conform to the spec.
# https://github.com/python/mypy/issues/12379
@overload # type: ignore[override]
def method(self, arg: Other | Sub) -> Sub: ...
@overload
def method(self, arg: Super) -> Super: ...