GeezerVison is showing github.com — Should `callable` narrow to `TypeIs[Callable[..., object]]` or `TypeIs[Callable]` ? · Issue #16050 · python/typeshed
Should callable narrow to TypeIs[Callable[..., object]] or TypeIs[Callable] ? #16050DescriptionCurrently, typeshed annotates
def callable(obj: object, /) -> TypeIs[Callable[..., object]]: ...
However, for the majority of type checkers this creates discrepancy between collections.abc.Callable and builtins.callable narrowing, only ty seems to consider blank Callable as if it were Callable[..., object], whereas all other type checkers treat it as Callable[..., Any].
from typing import reveal_type, TypeIs
from collections.abc import Callable
def is_callable(arg: object) -> TypeIs[Callable]:
raise NotImplementedError
def check_callable(arg: object) -> None:
if isinstance(arg, Callable):
reveal_type(arg)
def check_callable2(arg: object) -> None:
if callable(arg):
reveal_type(arg)
def check_callable3(arg: object) -> None:
if is_callable(arg):
reveal_type(arg)
| case |
isinstance(x, Callable) |
is_callable |
callable(x) |
| mypy |
ERROR |
(...) -> Any |
<callable subtype of object> |
| pyright |
(...) -> Unknown |
(...) -> Unknown |
(...) -> object |
| pyrefly |
(...) -> Unknown |
(...) -> Unknown |
(...) -> object |
| ty |
(...) -> object |
(...) -> Any |
(...) -> object |
| zuban |
ERROR |
(...) -> Any |
<callable subtype of object> |
ActivityApusBerliozi commented on Jul 20, 2026 Hey everyone!
Can I work on this? randolf-scholz commented on Jul 20, 2026 ContributorAuthor More actions @ApusBerliozi I already have a draft PR up, let's see what the mypy primer shows and what the maintainers say. JelleZijlstra commented on Jul 20, 2026 object is more correct; TypeIs here should ideally use the fully static type Top[Callable[..., object]], but it cannot be spelled in the current type system. That said, using Any as the return type may work better in practice at the moment.
Reacted by Randolf Scholz randolf-scholz commented on Jul 20, 2026 ContributorAuthor More actions Cross posting my comment from the PR, as it provides some relevant context:
Well, I would say there are 2 motivations:
-
I want if isinstance(arg, Callable): and if callable(arg): to produce identical narrowing. Whether or not thats Callable[..., Any] or Callable[..., object] is a second order concern.
-
I can see the motivation why one would want to infer object rather than Any/Unknown, but I do not see why one should special case that only for callable in particular. Consider:
from typing import reveal_type
from collections.abc import Iterable
def check(arg: object) -> None:
if isinstance(arg, Iterable):
reveal_type(arg)
reveal_type(next(iter(arg)))
Here all the type checkers, except ty, say it's Iterable[Any] / Iterable[unknown] as well. Hence, I don't believe it makes sense to single out and special case callable in typeshed. One could consider making all the standard covariant containers default to object, but maybe one should leave that to the type checker vendors. randolf-scholz commented on Jul 20, 2026 ContributorAuthor More actions @JelleZijlstra Thinking about it, doesn't it actually kind of depend on the object we are narrowing, if we take the gradual guarantee seriously?
from typing import Any
def check(arg: Any) -> int:
if callable(arg):
return arg()
return -1
if Any materializes to Callable[[], int] | None, then the code type checks. So, by the gradual guarantee this should type check.
On the other hand if we have had:
def check(arg: object) -> int:
if callable(arg):
return arg()
return -1
then yes, return arg() is likely an error. JelleZijlstra commented on Jul 20, 2026 The theoretically correct answer for your first code sample is Any & Top[Callable[..., object]], which is callable with any signature.
MetadataMetadataDevelopmentNo branches or pull requestsIssue actions
|