Skip to content

'Subclass of "A" and "B" cannot exist: would have incompatible method signatures' is usually wrong #19377

Open

Description

@JelleZijlstra

Mypy reports Subclass of "A" and "B" cannot exist: would have incompatible method signatures in many cases if given x: A, you run isinstance(x, B). However, this is often (always?) incorrect, as mypy does not account for subclasses that use method signatures that are subtypes of both classes.

Example (playground):

from typing import Never

class A:
    def f(self) -> int: return 0
class B:
    def f(self) -> str: return ""

class X(A, B):
    def f(self) -> Never: raise Exception

x: A = X()
if isinstance(x, B):  # E: Subclass of "A" and "B" cannot exist: would have incompatible method signatures
    reveal_type(x)    # E: Statement is unreachable
else:
    reveal_type(x)    # N: Revealed type is "__main__.A"

Here, mypy accepts the definition of a class X that is a subclass of A and B, yet also says that such a subclass cannot exist.

Any callable type is a subclass of (*args: object, **kwargs: object) -> Never, so incompatible method signatures cannot be a reason that a common subclass cannot exist.

It gets a little trickier with attributes. However, mypy accepts this class and then again claims it cannot exist (playground):

from typing import Never

class A:
    x: int
class B:
    x: str

class X(A, B):
    @property
    def x(self) -> Never: raise Exception
    @x.setter
    def x(self, x: int | str) -> None: pass

x: A = X()
if isinstance(x, B):  # E: Subclass of "A" and "B" cannot exist: would have incompatible method signatures
    reveal_type(x)    # E: Statement is unreachable
else:
    reveal_type(x)    # N: Revealed type is "__main__.A"

(Note though that pyright rejects this form of inheritance; possibly mypy should reject this too in the future if we revise how we treat inheritance of attributes.)

Activity

  1. sterliakov commented on Jul 5, 2025

    @sterliakov
    Collaborator

    I agree with the reasoning here. What's your ideal approach to this?

    In my experience this error usually grasps the intent correctly, detecting wrong isinstance checks during refactoring, and it is easy to silence otherwise (cast instead of ignore to avoid unreachable, but still). I'd really prefer to not see this check relaxed.

  2. JelleZijlstra commented on Jul 5, 2025

    @JelleZijlstra
    MemberAuthor

    Yes, pragmatically the current check is sometimes useful.

    ty implements a variation of this by mirroring the CPython runtime logic for builtin classes. This is essentially to figure the "solid base" for each base class, which is the first base class that has a different size (at the C level) than its parent, and to reject multiple inheritance if there are multiple solid bases involved. This approach has the advantage that multiple inheritance is rejected only if it would also be rejected at runtime. However, they hardcode the "solid base" status for a small set of builtins, so their approach doesn't generalize well to types outside the stdlib. We could generalize this if we added a way for stubs to represent the solid base status of classes.

    Mypy's current approach works sort of accidentally, in that most builtins that you might want to double-inherit from, and that are distinct solid bases, also happen to have methods that are incompatible with each other, so mypy rejects their double inheritance.

  3. ilevkivskyi commented on Jul 8, 2025

    @ilevkivskyi
    Member

    (Note though that pyright rejects this form of inheritance; possibly mypy should reject this too in the future if we revise how we treat inheritance of attributes.)

    And why is that? What is wrong with this example?

  4. JelleZijlstra commented on Jul 8, 2025

    @JelleZijlstra
    MemberAuthor

    And why is that? What is wrong with this example?

    Probably shouldn't have mentioned that; it's not really relevant to this issue and I'm not interested right now in discussing the finer points of attribute inheritance. The point of this issue is that if mypy simultaneously claims a subclass of these classes can't exist, yet allows one to exist.

  5. michalnik commented on Jul 24, 2025

    @michalnik

    Optional Parameters for Controlling Exceptions in @solid_base

    To increase flexibility and practical usability of the @solid_base decorator, I think about introducing optional parameters that allow fine-grained control over which classes the solid base restriction applies to.

    Two mutually exclusive parameters are suggested:

    enable: A list of classes for which the solid base restriction is enabled. The restriction applies only to these specified classes. Inheritance conflicts with other classes are allowed.

    disable: A list of classes for which the solid base restriction is disabled. The restriction applies to all classes except those listed here, allowing inheritance with them.

    This approach provides a clear and concise way to express exceptions and allows for modeling complex multiple inheritance scenarios with precise control.

    examples

    disable

    from typing import solid_base
    
    @solid_base(disable=[B])
    class A:
        ...
    
    @solid_base()
    class B:
        ...
    
    class C(A, B):  # Allowed because restriction is disabled for B in A
        ...

    enable

    @solid_base(enable=[B])
    class A:
        ...
    
    @solid_base()
    class B:
        ...
    
    class C(A, B):  # Allowed, restriction applies only to B
        ...
    
    class D(A, E):  # Disallowed if E is not in enable list
        ...
  6. kharybdys commented on Jul 24, 2025

    @kharybdys

    Sorry for butting in, I am following this discussion because I was also a contributor on the original issue.

    I assume @michalnik is brainstorming about possible solutions.

    But how is requiring the use of the solid base decorator any better than type ignore comments? I still need to clutter my codebase with stuff only because Mypy is not allowing a scenario that just works in Python. I'd rather do the type ignore then (but do fix it to one line instead of the current two per case).

    I'm still thinking Mypy should not complain about inconsistent init methods on anything with generated init methods, and should not complain at isinstance checks. If a is right now SomeClass, it still makes sense to do isinstance(a, OtherClass) in 100% of the cases because Mypy can't know if a shared subclass of both has been defined.

    For me that solves 100% of the cases this was bothering me. And if it solves 90% of the cases worldwide, that might be good enough

  7. JelleZijlstra commented on Jul 24, 2025

    @JelleZijlstra
    MemberAuthor

    @michalnik please post such ideas to the PEP discussion thread https://discuss.python.org/t/pep-800-solid-bases-in-the-type-system/99910, not here.

    @kharybdys I am not sure what cases are bothering you so I don't know how it relates to the issue.

  8. kharybdys commented on Jul 24, 2025

    @kharybdys

    I was uncertain whether it would be necessary, or useful, to repeat from the previous thread.

    Summary, I had a inheritance tree of Pydantic models, where I was also using a different Pydantic model as Mixin (no overlapping fields) in parts of the tree. I mostly remember isinstance checks on the Mixin causing 2 Unreachable errors - one on the isinstance and one on the first line after (outside) the if isinstance.

  9. michalnik commented on Jul 28, 2025

    @michalnik

    @michalnik please post such ideas to the PEP discussion thread https://discuss.python.org/t/pep-800-solid-bases-in-the-type-system/99910, not here.

    @kharybdys I am not sure what cases are bothering you so I don't know how it relates to the issue.

    Thank you for your response. I believe this is my first post related to a PEP, and it was the PEP itself that brought me here. Considering the nature of the other posts in this issue, I understand that mine might not belong here.
    Could I kindly ask you to repost my message on python.org? I don't have an account there, and I’d prefer not to create one just to share a single (hopefully useful) article.

  10. JelleZijlstra commented on Aug 18, 2025

    @JelleZijlstra
    MemberAuthor

    #19678 implements PEP 800, which can largely replace the current imprecise check in mypy.

    Concrete proposal: Mypy should add support for PEP 800 now and use it as an additional way to determine that two classes are disjoint (my PR). Once PEP 800 is accepted and widely implemented (e.g., typeshed uses it consistently), mypy should drop the existing "would have incompatible method signatures" check.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugmypy got something wrongtopic-inheritanceInheritance and incompatible overridestopic-reachabilityDetecting unreachable code

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions