[match-case]/[isinstance] narrowing logic against the special `AnyCallable` Protocol differs from `callable()` check
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 20.6k
- Forks
- 3.3k
- PR merge metrics
- PR metrics pending
Description
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
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by running the reproduction in the linked mypy-play example and compare callable(), isinstance(x, AnyCallable), and match-case narrowing. Trace the narrowing behavior for object, FnImpl, and Callable inputs; done means the protocol-based checks produce the same effective narrowing as callable() where the typing specification requires it.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- compilers, devtools
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 42/100