Order of additional overloads matters in subclass where it shouldnt [reportIncompatibleMethodOverride]
- Dominant language
- Python
- Stars
- 15.6k
- Forks
- 1.8k
- Avg merge
- 12h 13m
- Merged PRs (30d)
- 52
Description
**Describe the bug**
In some cases, a valid order of instance-method overloads will be (wrongly) rejected by pyright/pylance, but only if the implementing class inherits from a class that defines some of the overloads.
For example (see below for a complete source code including all imports etc) the following two classes are type hinted correctly (as expected):
```python
class ValidBase:
@overload
def map(self, x: str, wrap: Literal[True]) -> Tuple[str]: ...
@overload
def map(self, x: int, wrap: Literal[True]) -> Tuple[int]: ...
def map(self, x: str|int, wrap: Literal[True]) -> Tuple[str]|Tuple[int]: ...
class ValidComplete:
@overload
def map(self, x: str, wrap: Literal[False]=False) -> str: ...
@overload
def map(self, x: int, wrap: Literal[False]=False) -> int: ...
@overload
def map(self, x: str, wrap: Literal[True]) -> Tuple[str]: ...
@overload
def map(self, x: int, wrap: Literal[True]) -> Tuple[int]: ...
def map(self, x: str|int, wrap: bool=False) -> str|int|Tuple[str]|Tuple[int]: ...
```
Notice, that the two last overloads in `ValidComplete` are the same as in `ValidBase` and that every object inheriting from `ValidComplete` also completely matches the interface defined in `ValidBase`.
However, the following class, although being a 1:1 copy of `ValidComplete` (+ inheriting from `ValidBase`) and having the exact same (mutually exclusive) overloads on the `map(...)` method as `ValidBase` will show a type error:
```python
class InvalidDerived(ValidBase):
@overload
def map(self, x: str, wrap: Literal[False]=False) -> str: ...
@overload
def map(self, x: int, wrap: Literal[False]=False) -> int: ...
@overload
def map(self, x: str, wrap: Literal[True]) -> Tuple[str]: ...
@overload
def map(self, x: int, wrap: Literal[True]) -> Tuple[int]: ...
def map(self, x: str|int, wrap: bool=False) -> str|int|Tuple[str]|Tuple[int]: ... # -> type error saying that "map(...)" overrides "ValidBase" in an incompatible manner
```
Finally, if the two overload blocks are switched in the derived class, the type error disappears:
```python
class ValidDerived(ValidBase):
@overload
def map(self, x: str, wrap: Literal[True]) -> Tuple[str]: ...
@overload
def map(self, x: int, wrap: Literal[True]) -> Tuple[int]: ...
@overload
def map(self, x: str, wrap: Literal[False]=False) -> str: ...
@overload
def map(self, x: int, wrap: Literal[False]=False) -> int: ...
def map(self, x: str|int, wrap: bool=False) -> str|int|Tuple[str]|Tuple[int]: ... # -> no type error?
```
I think the following behaviours are wrong here:
1. `map(...)` on `InvalidDerived` should not be marked as invalid by pyright
2. The order between these mutually exclusive overloads should not matter, so if the behaviour of `InvalidDerived` is intended to be a type error, then `ValidDerived` should also be marked as invalid
**Code or Screenshots**
```python
from typing import Literal, Tuple, overload
# works as expected:
class ValidBase:
@overload
def map(self, x: str, wrap: Literal[True]) -> Tuple[str]: ...
@overload
def map(self, x: int, wrap: Literal[True]) -> Tuple[int]: ...
def map(self, x: str|int, wrap: Literal[True]) -> Tuple[str]|Tuple[int]: ...
# works as expected
class ValidComplete:
@overload
def map(self, x: str, wrap: Literal[False]=False) -> str: ...
@overload
def map(self, x: int, wrap: Literal[False]=False) -> int: ...
@overload
def map(self, x: str, wrap: Literal[True]) -> Tuple[str]: ...
@overload
def map(self, x: int, wrap: Literal[True]) -> Tuple[int]: ...
def map(self, x: str|int, wrap: bool=False) -> str|int|Tuple[str]|Tuple[int]: ...
# does not work, this is the issue i am reporting:
class InvalidDerived(ValidBase):
@overload
def map(self, x: str, wrap: Literal[False]=False) -> str: ...
@overload
def map(self, x: int, wrap: Literal[False]=False) -> int: ...
@overload
def map(self, x: str, wrap: Literal[True]) -> Tuple[str]: ...
@overload
def map(self, x: int, wrap: Literal[True]) -> Tuple[int]: ...
def map(self, x: str|int, wrap: bool=False) -> str|int|Tuple[str]|Tuple[int]: ... # -> type error saying that "map(...)" overrides "ValidBase" in an incompatible manner
# works, but i dont understand why the order between the overloads would matter?
class ValidDerived(ValidBase):
@overload
def map(self, x: str, wrap: Literal[True]) -> Tuple[str]: ...
@overload
def map(self, x: int, wrap: Literal[True]) -> Tuple[int]: ...
@overload
def map(self, x: str, wrap: Literal[False]=False) -> str: ...
@overload
def map(self, x: int, wrap: Literal[False]=False) -> int: ...
def map(self, x: str|int, wrap: bool=False) -> str|int|Tuple[str]|Tuple[int]: ... # -> no type error?
```
(roughly translates to)
> The method "map" overrides the class "ValidBase" in an incompatible manner.
> The override does not handle all overloads of the base method.
> Pylance(reportIncompatibleMethodOverride)
> Untitled-6(10, 9): Overriden method
**VS Code extension or command-line**
The screenshot was made in VSCode (1.127.0 Universal) with the latest pylance Plugin (2026.2.1) installed.
Contributor guide
Research direction
Start by running the complete Python example with pyright or Pylance and compare the diagnostics for InvalidDerived and ValidDerived. Trace the method-override overload checking path, then add coverage showing that mutually exclusive overload order does not change the result; done means equivalent derived declarations receive consistent diagnostics.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- devtools
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100