Skip to content

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

Closed as not planned
@Drino

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:

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

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:

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

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