Skip to content

Unexpected behavior on overloaded methods with annotated self of a generic class #18597

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
ro-oliveira95 opened this issue Feb 4, 2025 · 1 comment
Labels
bug mypy got something wrong

Comments

@ro-oliveira95
Copy link

Bug Report

Lately I discover that partial specialization is allowed by mypy by annotating self in the methods of a generic class, as shown in some examples in the docs (https://mypy.readthedocs.io/en/stable/more_types.html#advanced-uses-of-self-types), which is very nice. Then I went down trying to combine this with overloaded methods, but unfortunately it doesn't behaved as I expected. This got me wondering if it is a bug or if my implementation is lacking something. Any help understanding or fixing this would be very appreciated.

There is a few issues regarding this topic (e.g. #5320 and #10517), but they were facing slighted different problems and they are already closed/solved.

In summary, in the following example it seems that the second overload (def bar(self: Foo[bool], a: bytes) -> None:) is never reached, and there is some gotchas while calling self.bar from another method of Foo.

To Reproduce

from typing import *


MyType: TypeAlias = int | str | bool


T = TypeVar('T', bound=MyType, covariant=True)


class Foo(Generic[T]):
    
    def __init__(self, t: T) -> None:
        self.t = t
    
    def foo(self) -> T:
        return self.t
        
    @overload
    def bar(self: Foo[int | str], a: int) -> None: pass

    @overload
    def bar(self: Foo[bool], a: bytes) -> None: pass

    def bar(self, a: int | bytes) -> None: pass

    def calls_bar(self) -> None:
        self.bar(1)  # Not flagged.
        self.bar(b'ok')  # Flagged (expected "int"), but why?


# OK, all good.
Foo(1).bar(1)
Foo('ok').bar(1)
Foo(True).bar(b'ok')

# Not OK, but mypy doesn't flag 3rd one.
Foo(1).bar('not ok')
Foo('ok').bar(b'not ok')
Foo(True).bar(1)


def func(a: MyType) -> None:
    Foo(a).bar(b'ok')  # This is also flagged (expected "int")

Playground link: https://mypy-play.net/?mypy=latest&python=3.10&gist=466d7197598df9ca65258d8b7aca2c84

Expected Behavior

Among all the things a bit strange (to me) happening here, my main expectation is that mypy shoud have flagged Foo(True).bar(1), because it falls under the def bar(self: Foo[bool], a: bytes) -> None overload.

This tells me that the overload strategy is not working as I expected.

I'm also not sure the expected behavior of calling bar from other methods, such as what happens inside Foo.call_bar. At least I would expect both self.bar(1) and self.bar(b'ok') to fail or succeed, not only one.

One thing though regarding this last case: if I annotate self: Foo in call_bar, none of then are flagged -- I guess its because T will be Any so the call will fallback to the not-overloaded bar which accepts both types. I wonder if this would be the correct approach?

Actual Behavior

Mypy output:

main.py:28: error: Argument 1 to "bar" of "Foo" has incompatible type "bytes"; expected "int"  [arg-type]
main.py:37: error: Argument 1 to "bar" of "Foo" has incompatible type "str"; expected "int"  [arg-type]
main.py:38: error: Argument 1 to "bar" of "Foo" has incompatible type "bytes"; expected "int"  [arg-type]
main.py:43: error: Argument 1 to "bar" of "Foo" has incompatible type "bytes"; expected "int"  [arg-type]
Found 4 errors in 1 file (checked 1 source file)

Your Environment

  • Mypy version used: 1.14.1
  • Mypy command-line flags: N/A
  • Mypy configuration options from mypy.ini (and other config files): N/A
  • Python version used: 3.10
@ro-oliveira95 ro-oliveira95 added the bug mypy got something wrong label Feb 4, 2025
@ro-oliveira95
Copy link
Author

Ok now I realized my example is flawed: mypy is not flagging Foo(True).bar(1) because bool is a subtype of int, so the first overload [int | str] took precedence over the second bool.

After changing the bool to some other type all behaved as expected, so I'm closing this. Sorry for the noise :')

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

No branches or pull requests

1 participant