Skip to content

Type narrowing fails with isinstance(self, Protocol) when result is stored in a variable #19684

@semohr

Description

@semohr

Bug Report

When using a @runtime_checkable protocol and checking isinstance(self, MyProtocol), mypy does not correctly narrow the type of self if the isinstance result is first stored in a variable. This results in false positives when accessing protocol members, even though the code is valid at runtime.

To Reproduce

from abc import ABC
from typing import Protocol, runtime_checkable


@runtime_checkable
class MyProtocol(Protocol):
    def my_method(self, arg: int) -> str: ...


class Base(ABC):
    def foo(self) -> None:
        has_my_method = isinstance(self, MyProtocol)

        if has_my_method:
            # ❌ Mypy error:
            # test.py:17: error: "Base" has no attribute "my_method"  [attr-defined]
            self.my_method(42)

        if isinstance(self, MyProtocol):
            # ✅ Works fine here
            self.my_method(42)


class Extends(Base, MyProtocol):
    def my_method(self, arg: int) -> str:
        return f"Called with {arg}"

Expected Behavior

Both forms should refine self to MyProtocol inside the if block, since semantically the two checks are equivalent at runtime. Maybe I'm missing something here?

For example, the same pattern works fine if there is no self involved:

from typing import Protocol, runtime_checkable


@runtime_checkable
class MyProtocol(Protocol):
    def my_method(self, arg: int) -> str: ...


class MyClass:
    def my_method(self, arg: int) -> str:
        return f"Called with {arg}"


obj = MyClass()
has_my_method = isinstance(obj, MyProtocol)

if has_my_method:
    # ✅ No error
    obj.my_method(42)

Environment:
mypy 1.17.0 (compiled: yes)

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions