Skip to content

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

Closed
@ro-oliveira95

Description

@ro-oliveira95

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

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