Skip to content

[match-case]/[isinstance] narrowing logic against the special AnyCallable Protocol differs from callable() check #19470

Description

@randolf-scholz

According to the typing spec (https://typing.python.org/en/latest/spec/callables.html#meaning-of-in-callable),

@runtime_checkable
class AnyCallable(Protocol):
    def __call__(self, *args: Any, **kwargs: Any) -> Any: ...

is an implementation of Callable[..., Any]. Therefore, its isinstance-narrowing and match-case class pattern narrowing should behave the same as a simple if callable() narrowing. However, this is not the case: both isinstance and match-case produce different results:

from typing import Any, Callable
from typing_extensions import Protocol, runtime_checkable, assert_type

@runtime_checkable
class AnyCallable(Protocol):
    def __call__(self, *args: Any, **kwargs: Any) -> Any: ...

def check_proto(p: AnyCallable) -> None:
    assert_type(p, Callable[..., Any])

class FnImpl:
    def __call__(self, x: object, /) -> int: ...

# Test Baseline
def test_iscallable_object(x: object) -> None:
    if callable(x):
        reveal_type(x)  # N: Revealed type is "__main__.<callable subtype of object>"

def test_iscallable_impl(x: FnImpl) -> None:
    if callable(x):
        reveal_type(x)  # N: Revealed type is "__main__.FnImpl"

def test_iscallable_callable(x: Callable[[object], int]) -> None:
    if callable(x):
        reveal_type(x)  # N: Revealed type is "def (builtins.object) -> builtins.int"


# Test isinstance
def test_isinstance_object(x: object) -> None:
    if isinstance(x, AnyCallable):
        reveal_type(x)  # N: Revealed type is "__main__.AnyCallable"

def test_isinstance_impl(x: FnImpl) -> None:
    if isinstance(x, AnyCallable):
        reveal_type(x)  # N: Revealed type is "__main__.FnImpl"

def test_isinstance_callable(x: Callable[[object], int]) -> None:
    if isinstance(x, AnyCallable):
        reveal_type(x)  # N: Revealed type is "__main__.AnyCallable"


# test match-case
def test_match_object(x: object) -> None:
    match x:
        case AnyCallable() as fn:
            reveal_type(fn)  # N: Revealed type is "__main__.AnyCallable"

def test_match_impl(x: FnImpl) -> None:
    match x:
        case AnyCallable() as fn:
            reveal_type(fn)  # N: Revealed type is "__main__.AnyCallable"

def test_match_callable(x: Callable[[object], int]) -> None:
    match x:
        case AnyCallable() as fn:
            reveal_type(fn)  # N: Revealed type is "__main__.AnyCallable"

https://mypy-play.net/?mypy=latest&python=3.12&gist=5cb05553fa6674d445906eda99802e90

Activity

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 wrongtopic-match-statementPython 3.10's match statementtopic-type-narrowingConditional type narrowing / binder

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions