Description
Bug Report
A Callable
type alias is handled like usual generic, not as Callable
.
This does not allow to define a type alias for a decorator.
It can be handled via callback Protocol
, but in this case it is not possible to support signature transformation via ParamSpec
.
To Reproduce
Run mypy on following code:
from typing import Any, Callable, TypeAlias, TypeVar
from typing_extensions import reveal_type
TCallable = TypeVar('TCallable', bound=Callable[..., Any])
TDecorator: TypeAlias = Callable[[TCallable], TCallable]
def factory_with_unbound_type_alias() -> TDecorator:
...
def factory_with_bound_type_alias() -> TDecorator[TCallable]:
...
def some_function(request: Any) -> str:
return 'Hello world'
reveal_type(factory_with_unbound_type_alias()(some_function))
# note: Revealed type is "Any"
reveal_type(factory_with_bound_type_alias()(some_function))
# note: error: Argument 1 has incompatible type "Callable[[Any], str]"; expected <nothing>
# note: Revealed type is "<nothing>"
Expected Behavior
According to docs:
A parameterized generic alias is treated simply as an original type with the corresponding type variables substituted.
So def factory_with_bound_type_alias() -> TDecorator[TCallable]:
should work.
But to be honest this is pretty counterintuitive.
I'd expect:
reveal_type(factory_with_unbound_type_alias()(some_function))
# Revealed type is "def (request: Any) -> builtins.str"
Generally speaking I'd expect unbound type variables in Callable
type alias to be bound to its call scope, not filled with Any
.
It already works this way with Callable itself:
from typing import Any, Callable, Dict, TypeVar
from typing_extensions import reveal_type
T = TypeVar('T')
def callable_factory() -> Callable[[T], T]:
...
def dict_factory() -> Dict[T, T]:
...
reveal_type(callable_factory())
# note: Revealed type is "def [T] (T`-1) -> T`-1"
reveal_type(dict_factory())
# note: Revealed type is "builtins.dict[<nothing>, <nothing>]"
I'd expect it to work this way until alias has another non-callable generic depending on this variable (out of this Callable
scope), e.g. current behavior in this snippet is fine:
from typing import Any, Callable, List, Union, TypeVar
from typing_extensions import reveal_type
T = TypeVar('T')
TListOrFactory = Union[List[T], Callable[[], List[T]]]
def make_list_or_factory() -> TListOrFactory:
...
reveal_type(make_list_or_factory())
# note: Revealed type is "Union[builtins.list[Any], def () -> builtins.list[Any]]"
Your Environment
- Mypy version used: mypy 0.971
- Python version used: 3.10
Related discussion
I've created a similar ticket in Pyright repo:
microsoft/pyright#3803
It appears that the right way to handle Callable
in Pyright is by passing it a type variable:
def factory_with_bound_type_alias() -> TDecorator[TCallable]:
...
reveal_type(factory_with_bound_type_alias()(some_function))
# pyright: Type of "factory_with_bound_type_alias()(some_function)" is "(request: Any) -> str"
I've searched for any discussions on semantics of Callable aliases, but didn't manage to find anything.
So, after all I've created a (dead) discussion in typing repo:
python/typing#1236