Search code examples
pythonoverloadingpython-typingmypy

Is this a false positive [override] error? "Signature of (method) incompatible with supertype"


Although the method signature in Sub is compatible with Super, mypy rejects the override: Signature of "method" incompatible with supertype "Super".

I'm using

  • python 3.13.1
  • mypy 1.14.0

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

  • input arg of type Other|Super (= Other|Sub + Super) and
  • output type Super (= 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 and
  • Super 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?


P.S. Quick way to hide the error.

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.)


Solution

  • 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 callable A if at least one of the overloaded signatures in B is assignable to A

    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: ...