Skip to content

Self type with multiple inheritance #18458

Closed

Description

@jeremander

Not sure whether this is actually a bug or not, but I'm getting a base class incompatibility error when inheriting from two classes defining a method with the same signature containing the typing.Self type.

To Reproduce

from typing import Self

class A:
    def a_method(self) -> None:
        pass
    def method(self: Self, other: Self) -> None:
        self.a_method()
        other.a_method()

class B:
    def b_method(self) -> None:
        pass
    def method(self: Self, other: Self) -> None:
        self.b_method()
        other.b_method()

class C(A, B):
    pass

Running mypy gives this error:

test.py:17: error: Definition of "method" in base class "A" is incompatible with definition in base class "B"  [misc]

I expected Self to mean "whatever this type happens to be" (base class or subclass) but the error would suggest that it's interpreting Self as the specific class containing that method definition.

Next I tried the following variation:

from typing import TypeVar

T = TypeVar('T')

class A:
    def a_method(self) -> None:
        pass
    def method(self: T, other: T) -> None:
        self.a_method()

class B:
    def b_method(self) -> None:
        pass
    def method(self: T, other: T) -> None:
        self.b_method()

class C(A, B):
    pass

This fixes the incompatibility error but results in new errors:

test.py:9: error: "T" has no attribute "a_method"  [attr-defined]
test.py:15: error: "T" has no attribute "b_method"  [attr-defined]

I also tried using two separate TypeVars, one with bound='A' and the other with bound='B', but this brought back the "incompatible definition" error from before.

Finally (after reverting the types back to Self), I updated C's definition to the following:

class C(A, B):
    def method(self: Self, other: Self) -> None:
        A.method(self, other)

This ended up type-checking, but I'm wondering if mypy ought to be clever enough to figure it out without having to explicitly override method, or if there's some other alternative?

And if not, perhaps the error message about the incompatible definition could elaborate a bit more in the case where the type signatures "appear" to match due to the use of Self?

Environment

  • Python 3.11.11
  • mypy 1.14.1 (no flags or config file)

Activity

  1. sterliakov commented on Jan 14, 2025

    @sterliakov
    Collaborator

    This is trivial to fix (as far as I understand, that's just a bug, and active_self_type is None when checking base class compatibility - but it has nothing to do with scope, sub_info is already the correct typeinfo, and free vars aren't important here?)

    diff --git a/mypy/checker.py b/mypy/checker.py
    index 79d178f3c..06e31cddd 100644
    --- a/mypy/checker.py
    +++ b/mypy/checker.py
    @@ -2232,8 +2232,8 @@ class TypeChecker(NodeVisitor[None], CheckerPluginInterface):
                     is_class_method = sym.node.is_class
     
                 mapped_typ = cast(FunctionLike, map_type_from_supertype(typ, sub_info, super_info))
    -            active_self_type = self.scope.active_self_type()
    -            if isinstance(mapped_typ, Overloaded) and active_self_type:
    +            active_self_type = fill_typevars(sub_info)
    +            if isinstance(mapped_typ, Overloaded):
                     # If we have an overload, filter to overloads that match the self type.
                     # This avoids false positives for concrete subclasses of generic classes,
                     # see testSelfTypeOverrideCompatibility for an example.

    However, Self in argument position still bugs me. It's inherently unsafe and/or underspecified.

    To be clear, minimal unsafety illustration (crashes at runtime, accepted both by mypy and pyright):

    from typing import Self
    
    class A:
        def fn(self, other: Self) -> int:
           return 0
    
    class B(A):
        foo: int = 0
    
        def fn(self, other: Self) -> int:
            return other.foo
    
    def handle(obj: A) -> None:
        obj.fn(A())
    
    handle(B())

    We need a better approach to Self type in method arguments (actually IMO we need to prohibit it entirely, but I probably won't be heard).

  2. ilevkivskyi commented on Jan 14, 2025

    @ilevkivskyi
    Member

    but I probably won't be heard

    From the very beginning we new that self types are inherently unsafe. It was kind of a conscious trade-off, as otherwise they will be a pain to use.

  3. added a commit that references this issue on Jan 14, 2025
    b68c545
  4. sterliakov commented on Jan 14, 2025

    @sterliakov
    Collaborator

    This very problem is not about Self, it can be reproduced with explicit typevars. This is obviously a usability+implementation complexity vs strictness tradeoff, and perhaps current approach is the most ergonomic one (though I'd prefer to reject Self and its typevar-based counterpart in any contravariant position in overrides, and so just stay away from Self in my own code entirely except for factory classmethods).

  5. ilevkivskyi commented on Jan 15, 2025

    @ilevkivskyi
    Member

    This very problem is not about Self, it can be reproduced with explicit typevars.

    When I say from the very beginning I mean from the very beginning. What you call "with explicit typevars" is also called self-types, since it works not just because this is how type variables work, there are specific code paths that were added to support this pattern, note the title of #2193 (and this is btw how Self is implemented internally, it is just transformed into a type variable).

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 wrong

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions