Skip to content

Unbound type variables in Callable type alias are substituted with Any #13449

Description

@Drino

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:

fromtypingimportAny, Callable, TypeAlias, TypeVarfromtyping_extensionsimportreveal_typeTCallable=TypeVar('TCallable', bound=Callable[..., Any])
TDecorator: TypeAlias=Callable[[TCallable], TCallable]
deffactory_with_unbound_type_alias() ->TDecorator:
...
deffactory_with_bound_type_alias() ->TDecorator[TCallable]:
...
defsome_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:

fromtypingimportAny, Callable, Dict, TypeVarfromtyping_extensionsimportreveal_typeT=TypeVar('T')
defcallable_factory() ->Callable[[T], T]:
...
defdict_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:

fromtypingimportAny, Callable, List, Union, TypeVarfromtyping_extensionsimportreveal_typeT=TypeVar('T')
TListOrFactory=Union[List[T], Callable[[], List[T]]]
defmake_list_or_factory() ->TListOrFactory:
...
reveal_type(make_list_or_factory()) # note: Revealed type is "Union[builtins.list[Any], def () -> builtins.list[Any]]"

Your Environment

gist
mypy-playground

  • 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:

deffactory_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

Metadata

Metadata

Assignees

Labels

bugmypy got something wrongtopic-type-aliasTypeAlias and other type alias issues

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions