python / python/mypy

[overload][intersection-types] undetected LSP violation in multiple inheritance

Open
#15,790 4 comments 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

bug
Dominant language
Python
Stars
20.6k
Forks
3.3k
PR merge metrics
PR metrics pending

Description

Bug Report

Essentially, the bug occurs when trying to define a subclass of C≤A&B, and implementing a shared method via @overload:

class A:
    def foo(self, x: Hashable) -> Hashable: ...
class B:
    def foo(self, x: Sized) -> Sized: ...
class C(A, B):
    @overload
    def foo(self, x: Hashable) -> Hashable: ...
    @overload
    def foo(self, x: Sized) -> Sized: ...

C is not a proper subclass of A & B since C.foo(Hashable & Sized) yields Hashable, violating LSP for B. mypy does not detect this issue. To resolve this, an extra overload of the form def foo(x: Hashable & Sized) -> ... is needed.

To Reproduce

A concrete demo with non-trivial types.
from typing import overload
from collections.abc import Hashable, Sized

class A:
    def foo(self, x: Hashable) -> int: 
        return hash(x)
    
class B:
    def foo(self, x: Sized) -> str: 
        return f"x has {len(x)} elements"
    
class C(A, B):
    @overload
    def foo(self, x: Hashable) -> int: ...
    @overload
    def foo(self, x: Sized) -> str: ... 
    
    def foo(self, x):  # type: ignore[no-untyped-def]
        if isinstance(x, Hashable):
            return A.foo(self, x)
        if isinstance(x, Sized):
            return B.foo(self, x)

# mypy does not complain about LSP
a_lsp: A = C()
b_lsp: B = C()
c: C = C()

# tuples are hashable and sized.
x: tuple[int, int, int] = (1,2,3)

# but the return types are incompatible!
reveal_type(a_lsp.foo(x))  # int
reveal_type(b_lsp.foo(x))  # str
reveal_type(c.foo(x))      # int, is not a subtype of str

This file parses even using --strict without raising any warnings/errors. (mypy-playground)
However, the inconsistency becomes obvious as c.foo(<tuple>) reveals as int, which is incompatible with b.foo(<tuple>).

Expected Behavior

@overload
def foo(self, x: Hashable) -> int: ...
@overload
def foo(self, x: Sized) -> str: ... 

Are not type-safe overloads to satisfy LSP for both superclasses. Mypy should notice this and issue a warning. Likely, to solve this issue in a satisfactory manner, intersection-types are needed (https://github.com/python/typing/issues/213).

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reproducing the supplied multiple-inheritance example with mypy under --strict, using the linked mypy-playground case and reveal_type results. Trace overload resolution and inherited-method compatibility to determine where the LSP violation is missed; done means the behavior is addressed with a clear diagnostic or an explicitly documented limitation.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
compilers, devtools
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.