Skip to content

Self type with multiple inheritance #18458

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
jeremander opened this issue Jan 14, 2025 · 4 comments · Fixed by #18465
Closed

Self type with multiple inheritance #18458

jeremander opened this issue Jan 14, 2025 · 4 comments · Fixed by #18465
Labels
bug mypy got something wrong

Comments

@jeremander
Copy link

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)
@sterliakov
Copy link
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).

@ilevkivskyi
Copy link
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.

@sterliakov
Copy link
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).

@ilevkivskyi
Copy link
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
Labels
bug mypy got something wrong
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants