crates/ty_python_semantic/resources/mdtest/narrow/callable.md
callable()The callable() builtin returns TypeIs[Callable[..., object]], which narrows the type to the
intersection with Top[Callable[..., object]]. The Top[...] wrapper indicates this is a fully
static type representing the top materialization of a gradual callable.
Since all callable types are subtypes of Top[Callable[..., object]], intersections with Top[...]
simplify to just the original callable type.
from typing import Any, Callable
def f(x: Callable[..., Any] | None):
if callable(x):
# The intersection simplifies because `(...) -> Any` is a subtype of
# `Top[(...) -> object]` - all callables are subtypes of the top materialization.
reveal_type(x) # revealed: (...) -> Any
else:
# Since `(...) -> Any` is a subtype of `Top[(...) -> object]`, the intersection
# with the negation is empty (Never), leaving just None.
reveal_type(x) # revealed: None
from typing import Any, Callable
def g(x: Callable[[int], str] | None):
if callable(x):
# All callables are subtypes of `Top[(...) -> object]`, so the intersection simplifies.
reveal_type(x) # revealed: (int, /) -> str
else:
reveal_type(x) # revealed: None
def h(x: Callable[..., int] | None):
if callable(x):
reveal_type(x) # revealed: (...) -> int
else:
reveal_type(x) # revealed: None
def f(x: object):
if callable(x):
reveal_type(x) # revealed: Top[(...) -> object]
else:
reveal_type(x) # revealed: ~Top[(...) -> object]
The narrowed type Top[Callable[..., object]] represents the set of all possible callable types
(including, e.g., functions that take no arguments and functions that require arguments). While such
objects are callable (they pass callable()), no specific set of arguments can be guaranteed to
be valid.
import typing as t
def call_with_args(y: object, a: int, b: str) -> object:
if isinstance(y, t.Callable):
# error: [call-top-callable]
return y(a, b)
return None
If a top-callable is part of an intersection, it should still contribute its return type even when the other intersection elements are not callable:
def resolve(value: str):
if callable(value):
reveal_type(value) # revealed: str & Top[(...) -> object]
# error: [call-top-callable]
reveal_type(value()) # revealed: object
When callable() is used with a named expression, the target of the named expression should be
narrowed.
from typing import Any
class Foo:
func: Any | None
def f(foo: Foo):
first = getattr(foo, "func", None)
if callable(first):
reveal_type(first) # revealed: Any & Top[(...) -> object]
else:
reveal_type(first) # revealed: (Any & ~Top[(...) -> object]) | None
if callable(second := getattr(foo, "func", None)):
reveal_type(second) # revealed: Any & Top[(...) -> object]
else:
reveal_type(second) # revealed: (Any & ~Top[(...) -> object]) | None
A narrowed callable Top[Callable[..., object]] should be assignable to Callable[..., Any]. This
is important for decorators and other patterns where we need to pass the narrowed callable to
functions expecting gradual callables.
from typing import Any, Callable, TypeVar
from ty_extensions import static_assert, Top, is_assignable_to
static_assert(is_assignable_to(Top[Callable[..., bool]], Callable[..., int]))
F = TypeVar("F", bound=Callable[..., Any])
def wrap(f: F) -> F:
return f
def f(x: object):
if callable(x):
# x has type `Top[(...) -> object]`, which should be assignable to `Callable[..., Any]`
wrap(x)
isinstance parity for typing.Callable and collections.abc.Callabletyping.Callable is a deprecated alias for collections.abc.Callable. Both should narrow
identically when used as the second argument to isinstance().
import typing
import collections.abc
def f(x: object):
if isinstance(x, typing.Callable):
reveal_type(x) # revealed: Top[(...) -> object]
if isinstance(x, collections.abc.Callable):
reveal_type(x) # revealed: Top[(...) -> object]
Callable special-form identitytyping.Callable and collections.abc.Callable are both modeled as special forms. Import
resolution should preserve which module the symbol comes from, even when the symbol is re-exported
through another module. These tests only check symbol resolution; class-pattern behavior is tested
separately below.
import collections.abc
import typing
from collections.abc import Callable as CollectionsAbcCallable
from typing import Callable as TypingCallable
from _collections_abc import Callable as _CollectionsAbcCallable
reveal_type(TypingCallable) # revealed: <special-form 'typing.Callable'>
reveal_type(typing.Callable) # revealed: <special-form 'typing.Callable'>
reveal_type(CollectionsAbcCallable) # revealed: <special-form 'collections.abc.Callable'>
reveal_type(collections.abc.Callable) # revealed: <special-form 'collections.abc.Callable'>
reveal_type(_CollectionsAbcCallable) # revealed: <special-form 'collections.abc.Callable'>
typing_compat.py:
from typing import Callable
collections_abc_compat.py:
from collections.abc import Callable
main.py:
from collections_abc_compat import Callable as CollectionsAbcCallable
from typing_compat import Callable as TypingCallable
reveal_type(TypingCallable) # revealed: <special-form 'typing.Callable'>
reveal_type(CollectionsAbcCallable) # revealed: <special-form 'collections.abc.Callable'>
typing.Callable and collections.abc.CallableAt runtime, collections.abc.Callable is supported in match statement class patterns, however
typing.Callable is not.
collections.abc.Callableimport collections.abc
def _(subj: int | collections.abc.Callable[..., str]) -> None:
match subj:
# TODO: Should be valid.
# error: [invalid-match-pattern] "`<special-form 'collections.abc.Callable'>` cannot be used in a class pattern because it is not a type"
case collections.abc.Callable(): ...
case _: ...
typing.Callableimport typing
def _(subj: int | typing.Callable[..., str]) -> None:
match subj:
# error: [invalid-match-pattern] "`<special-form 'typing.Callable'>` cannot be used in a class pattern because it is not a type"
case typing.Callable(): ...
case _: ...