crates/ty_python_semantic/resources/mdtest/annotations/callable.md
References:
Note that typing.Callable is deprecated at runtime, in favor of collections.abc.Callable (see:
https://docs.python.org/3/library/typing.html#deprecated-aliases). However, removal of
typing.Callable is not currently planned, and the canonical location of the stub for the symbol in
typeshed is still typing.pyi.
The Callable special form requires exactly two arguments where the first argument is either a
parameter type list, parameter specification, typing.Concatenate, or ... and the second argument
is the return type. Here, we explore various invalid forms.
A bare Callable without any type arguments:
from typing import Callable, Any
from ty_extensions import is_equivalent_to, static_assert
def _(c: Callable):
reveal_type(c) # revealed: (...) -> Unknown
static_assert(is_equivalent_to(Callable, Callable[..., Any]))
When it's not a list:
from typing import Callable
# error: [invalid-type-form] "The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...`"
def _(c: Callable[int, str]):
reveal_type(c) # revealed: (...) -> Unknown
Or, when it's a literal type:
# error: [invalid-type-form] "The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...`"
def _(c: Callable[42, str]):
reveal_type(c) # revealed: (...) -> Unknown
Or, when one of the parameter type is invalid in the list:
# error: [invalid-type-form] "Int literals are not allowed in this context in a parameter annotation"
# error: [invalid-type-form] "Boolean literals are not allowed in this context in a parameter annotation"
def _(c: Callable[[int, 42, str, False], None]):
# revealed: (int, Unknown, str, Unknown, /) -> None
reveal_type(c)
Or, when an ellipsis literal is used as a parameter type in the list (note that the valid gradual
form uses ... as the entire first argument, not inside a list):
# error: [invalid-type-form] "`[...]` is not a valid parameter list for `Callable`: Did you mean `Callable[..., int]`?"
def _(c: Callable[[...], int]):
reveal_type(c) # revealed: (...) -> int
# error: [invalid-type-form] "`...` is not allowed in this context in a parameter annotation"
def _(c: Callable[[int, ...], int]):
reveal_type(c) # revealed: (int, Unknown, /) -> int
Using a parameter list:
from typing import Callable
# error: [invalid-type-form] "Special form `typing.Callable` expected exactly two arguments (parameter types and return type)"
def _(c: Callable[[int, str]]):
reveal_type(c) # revealed: (...) -> Unknown
Or, an ellipsis:
# error: [invalid-type-form] "Special form `typing.Callable` expected exactly two arguments (parameter types and return type)"
def _(c: Callable[...]):
reveal_type(c) # revealed: (...) -> Unknown
Or something else that's invalid in a type expression generally:
# fmt: off
def _(c: Callable[ # error: [invalid-type-form] "Special form `typing.Callable` expected exactly two arguments (parameter types and return type)"
{1, 2} # error: [invalid-type-form] "The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...`"
]
):
reveal_type(c) # revealed: (...) -> Unknown
from typing import Callable
# fmt: off
def _(c: Callable[
# error: [invalid-type-form] "Int literals are not allowed in this context in a parameter annotation"
{1, 2}, 2 # error: [invalid-type-form] "The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...`"
]
):
reveal_type(c) # revealed: (...) -> Unknown
We can't reliably infer the callable type if there are more then 2 arguments because we don't know which argument corresponds to either the parameters or the return type.
from typing import Callable
# error: [invalid-type-form] "Special form `typing.Callable` expected exactly two arguments (parameter types and return type)"
def _(c: Callable[[int], str, str]):
reveal_type(c) # revealed: (...) -> Unknown
from typing import Callable
# fmt: off
def _(c: Callable[
int, # error: [invalid-type-form] "The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...`"
[str] # error: [invalid-type-form] "List literals are not allowed in this context in a parameter annotation"
]
):
reveal_type(c) # revealed: (...) -> Unknown
from typing import Callable
# fmt: off
def _(c: Callable[
int, # error: [invalid-type-form] "The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...`"
(str, ) # error: [invalid-type-form] "Tuple literals are not allowed in this context in a parameter annotation"
]
):
reveal_type(c) # revealed: (...) -> Unknown
from typing import Callable
# error: [invalid-type-form] "List literals are not allowed in this context in a parameter annotation"
def _(c: Callable[[int], [str]]):
reveal_type(c) # revealed: (int, /) -> Unknown
from typing import Callable
# fmt: off
def _(c: Callable[ # error: [invalid-type-form] "Special form `typing.Callable` expected exactly two arguments (parameter types and return type)"
[int],
[str], # error: [invalid-type-form] "List literals are not allowed in this context in a parameter annotation"
[bytes] # error: [invalid-type-form] "List literals are not allowed in this context in a parameter annotation"
]
):
reveal_type(c) # revealed: (...) -> Unknown
A simple Callable with multiple parameters and a return type:
from typing import Callable
def _(c: Callable[[int, str], int]):
reveal_type(c) # revealed: (int, str, /) -> int
An overloaded callable should be assignable to a non-overloaded callable type when the overload set as a whole is compatible with the target callable.
from typing import Callable, overload
@overload
def foo(x: int) -> str: ...
@overload
def foo(x: str) -> str: ...
def foo(x: int | str) -> str:
return str(x)
def expects(c: Callable[[int | str], str]) -> None:
pass
expects(foo)
from typing import overload
@overload
def foo(x: int) -> str: ...
@overload
def foo(x: str) -> str: ...
def foo(x: int | str) -> str:
return str(x)
def errors() -> None:
for x in map(foo, range(1, 10)):
print(x)
from typing import Callable, overload
@overload
def converter(x: int) -> str: ...
@overload
def converter(x: bytes) -> bytes: ...
def converter(x: int | bytes) -> str | bytes:
if isinstance(x, int):
return str(x)
return x
def expects_int_str(c: Callable[[int], str]) -> None:
pass
expects_int_str(converter)
The overload set must cover the full target parameter domain.
from typing import Callable, overload
@overload
def partial_converter(x: int) -> str: ...
@overload
def partial_converter(x: bytes) -> str: ...
def partial_converter(x: int | bytes) -> str:
return str(x)
def expects_int_or_str(c: Callable[[int | str], str]) -> None:
pass
# error: [invalid-argument-type]
expects_int_or_str(partial_converter)
Even when the parameter domain is covered, return compatibility must still hold.
from typing import Callable, overload
@overload
def wide_return_converter(x: int) -> str: ...
@overload
def wide_return_converter(x: str) -> bytes: ...
def wide_return_converter(x: int | str) -> str | bytes:
if isinstance(x, int):
return str(x)
return x.encode()
def expects_str_return(c: Callable[[int | str], str]) -> None:
pass
# error: [invalid-argument-type]
expects_str_return(wide_return_converter)
from typing import Callable, Union
def _(
c: Callable[[Union[int, str]], int] | None,
d: None | Callable[[Union[int, str]], int],
e: None | Callable[[Union[int, str]], int] | int,
):
reveal_type(c) # revealed: ((int | str, /) -> int) | None
reveal_type(d) # revealed: None | ((int | str, /) -> int)
reveal_type(e) # revealed: None | ((int | str, /) -> int) | int
from typing import Callable, Union
from ty_extensions import Intersection, Not
class Foo: ...
def _(
c: Intersection[Callable[[Union[int, str]], int], int],
d: Intersection[int, Callable[[Union[int, str]], int]],
e: Intersection[int, Callable[[Union[int, str]], int], Foo],
f: Intersection[Not[Callable[[int, str], Intersection[int, Foo]]]],
):
reveal_type(c) # revealed: ((int | str, /) -> int) & int
reveal_type(d) # revealed: int & ((int | str, /) -> int)
reveal_type(e) # revealed: int & ((int | str, /) -> int) & Foo
reveal_type(f) # revealed: ~((int, str, /) -> int & Foo)
A nested Callable as one of the parameter types:
from typing import Callable
def _(c: Callable[[Callable[[int], str]], int]):
reveal_type(c) # revealed: ((int, /) -> str, /) -> int
And, as the return type:
def _(c: Callable[[int, str], Callable[[int], int]]):
reveal_type(c) # revealed: (int, str, /) -> ((int, /) -> int)
The Callable special form supports the use of ... in place of the list of parameter types. This
is a gradual form indicating that the type is consistent with any input signature:
from typing import Callable
def gradual_form(c: Callable[..., str]):
reveal_type(c) # revealed: (...) -> str
typing.ConcatenateUsing Concatenate as the first argument to Callable:
from typing_extensions import Callable, Concatenate
def _(c: Callable[Concatenate[int, str, ...], int]):
reveal_type(c) # revealed: (int, str, /, *args: Any, **kwargs: Any) -> int
Other type expressions can be nested inside Concatenate:
def _(c: Callable[Concatenate[int | str, type[str], ...], int]):
reveal_type(c) # revealed: (int | str, type[str], /, *args: Any, **kwargs: Any) -> int
But providing fewer than 2 arguments to Concatenate is an error:
# fmt: off
def _(
c: Callable[Concatenate[int], int], # error: [invalid-type-form] "`typing.Concatenate` requires at least 2 arguments when used in a type expression (got 1)"
d: Callable[Concatenate[(int,)], int], # error: [invalid-type-form] "`typing.Concatenate` requires at least 2 arguments when used in a type expression (got 1)"
e: Callable[Concatenate[()], int] # error: [invalid-type-form] "`typing.Concatenate` requires at least 2 arguments when used in a type expression (got 0)"
):
reveal_type(c) # revealed: (...) -> int
reveal_type(d) # revealed: (...) -> int
reveal_type(e) # revealed: (...) -> int
# fmt: on
typing.ParamSpec[environment]
python-version = "3.12"
Using a ParamSpec in a Callable annotation:
from typing_extensions import Callable
def _[**P1](c: Callable[P1, int]):
reveal_type(P1.args) # revealed: P1@_.args
reveal_type(P1.kwargs) # revealed: P1@_.kwargs
reveal_type(c) # revealed: (**P1@_) -> int
And, using the legacy syntax:
from typing_extensions import ParamSpec
P2 = ParamSpec("P2")
def _(c: Callable[P2, int]):
reveal_type(c) # revealed: (**P2@_) -> int
typing.UnpackUsing the unpack operator (*):
from typing_extensions import Callable, TypeVarTuple
Ts = TypeVarTuple("Ts")
def _(c: Callable[[int, *Ts], int]):
# TODO: Should reveal the correct signature
reveal_type(c) # revealed: (...) -> int
And, using the legacy syntax using Unpack:
from typing_extensions import Unpack
def _(c: Callable[[int, Unpack[Ts]], int]):
# TODO: Should reveal the correct signature
reveal_type(c) # revealed: (...) -> int
from typing import Callable
def _(c: Callable[[int], int]):
reveal_type(c.__init__) # revealed: bound method object.__init__() -> None
reveal_type(c.__class__) # revealed: type
reveal_type(c.__call__) # revealed: (int, /) -> int
Unlike other type checkers, we do not allow attributes to be accessed that would only be available on function-like callables:
def f_wrong(c: Callable[[], None]):
# error: [unresolved-attribute] "Object of type `() -> None` has no attribute `__qualname__`"
c.__qualname__
# error: [unresolved-attribute] "Unresolved attribute `__qualname__` on type `() -> None`"
c.__qualname__ = "my_callable"
We do this, because at runtime, calls to f_wrong with a non-function callable would raise an
AttributeError:
class MyCallable:
def __call__(self) -> None:
pass
f_wrong(MyCallable()) # raises `AttributeError` at runtime
If users want to read/write to attributes such as __qualname__, they need to check the existence
of the attribute first:
from inspect import getattr_static
def f_okay(c: Callable[[], None]):
if hasattr(c, "__qualname__"):
reveal_type(c.__qualname__) # revealed: object
# TODO: should be `property`
# (or complain that we don't know that `type(c)` has the attribute at all!)
reveal_type(type(c).__qualname__) # revealed: @Todo(Intersection meta-type)
# `hasattr` only guarantees that an attribute is readable.
#
# error: [invalid-assignment] "Object of type `Literal["my_callable"]` is not assignable to attribute `__qualname__` on type `(() -> None) & <Protocol with members '__qualname__'>`"
c.__qualname__ = "my_callable"
result = getattr_static(c, "__qualname__")
reveal_type(result) # revealed: property
if isinstance(result, property) and result.fset:
c.__qualname__ = "my_callable" # okay
from ty_extensions import into_regular_callable
class Base:
def __init__(self) -> None:
pass
class A(Base):
pass
# revealed: () -> A
reveal_type(into_regular_callable(A))