Skip to content

Should callable narrow to TypeIs[Callable[..., object]] or TypeIs[Callable] ? #16050

Description

@randolf-scholz

Currently, 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>

Activity

  1. ApusBerliozi commented on Jul 20, 2026

    @ApusBerliozi
    Contributor

    Hey everyone!

    Can I work on this?

  2. randolf-scholz commented on Jul 20, 2026

    @randolf-scholz
    ContributorAuthor

    @ApusBerliozi I already have a draft PR up, let's see what the mypy primer shows and what the maintainers say.

  3. JelleZijlstra commented on Jul 20, 2026

    @JelleZijlstra
    Member

    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.

  4. randolf-scholz commented on Jul 20, 2026

    @randolf-scholz
    ContributorAuthor

    Cross posting my comment from the PR, as it provides some relevant context:


    Well, I would say there are 2 motivations:

    1. 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.

    2. 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.

  5. randolf-scholz commented on Jul 20, 2026

    @randolf-scholz
    ContributorAuthor

    @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.

  6. JelleZijlstra commented on Jul 20, 2026

    @JelleZijlstra
    Member

    The theoretically correct answer for your first code sample is Any & Top[Callable[..., object]], which is callable with any signature.

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

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions