Skip to content

Bug: issubclass() doesn't narrow TypeVar type with Protocol #8556

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
antonagestam opened this issue Mar 18, 2020 · 2 comments · Fixed by #19183
Closed

Bug: issubclass() doesn't narrow TypeVar type with Protocol #8556

antonagestam opened this issue Mar 18, 2020 · 2 comments · Fixed by #19183
Labels
false-positive mypy gave an error on correct code feature priority-2-low topic-type-narrowing Conditional type narrowing / binder topic-type-variables

Comments

@antonagestam
Copy link
Contributor

Using issubclass with a runtime checkable Protocol and a union-bound TypeVar fails to narrow the type. The below example is from invoking mypy with mypy --python-version=3.8 reproduce-issubclass-bug.py. The issue seems similar to #7920. Tested on both 0.770 and master (0.770+dev.afe0667bbe85e048b6bb4e3fc1b7d5a375b7d01f).

from typing import Any
from typing import Protocol
from typing import runtime_checkable
from typing import Type
from typing import TypeVar
from typing import Union

T = TypeVar("T", bound=Any)


@runtime_checkable
class Parsable(Protocol):
    @classmethod
    def parse(cls: Type[T], data: Any) -> T:
        ...


R = TypeVar("R", bound=Union[Parsable, str])


def parse_object(response_type: Type[R], item: Any) -> R:
    if issubclass(response_type, Parsable):
        # note: Revealed type is 'Type[reproduce-issubclass-bug.Parsable]'
        reveal_type(response_type)
        # error: Incompatible return value type (got "Parsable", expected "R")
        return response_type.parse(item)
    # note: Revealed type is 'Type[R`-1]'
    reveal_type(response_type)
    # error: Too many arguments for "Parsable"
    # error: Incompatible return value type (got "Union[Parsable, str]", expected "R")
    return response_type(item)

Output:

reproduce-issubclass-bug.py:25: note: Revealed type is 'Type[reproduce-issubclass-bug.Parsable]'
reproduce-issubclass-bug.py:27: error: Incompatible return value type (got "Parsable", expected "R")
reproduce-issubclass-bug.py:29: note: Revealed type is 'Type[R`-1]'
reproduce-issubclass-bug.py:32: error: Too many arguments for "Parsable"
reproduce-issubclass-bug.py:32: error: Incompatible return value type (got "Union[Parsable, str]", expected "R")
Found 3 errors in 1 file (checked 1 source file)
@JukkaL
Copy link
Collaborator

JukkaL commented Mar 20, 2020

The narrowing seems to work to a certain extent, but mypy loses the correspondence between the type of response_type and the type variable R. This may be too difficult to for mypy to handle, so a # type: ignore or a cast will likely be required in the first return statement.

After the if statement, mypy could figure out that the type should be narrowed. However, again mypy doesn't have the notion of narrowed, dependent type variable, which would be required here.

So this would be nice to support, but it seems too complicated to be worth implementing. I'm keeping this issue open, however, in case there are other reports of this issue, or if somebody comes up with a simple way of implementing this.

@antonagestam
Copy link
Contributor Author

@JukkaL Gotcha, thanks for elaborating!

I think I'll manage to work around this by splitting the TypeVar into two and using an overload.

from typing import Any
from typing import Protocol
from typing import runtime_checkable
from typing import Type
from typing import TypeVar
from typing import Union
from typing import overload

T = TypeVar("T", bound=Any)


@runtime_checkable
class Parsable(Protocol):
    @classmethod
    def parse(cls: Type[T], data: Any) -> T:
        ...


RP = TypeVar("RP", bound=Parsable)
RB = TypeVar("RB", bound=str)
# for other functions using the original typevar
R = Union[RP, RB]


@overload
def parse_object(response_type: Type[RP], item: Any) -> RP: ...

@overload
def parse_object(response_type: Type[RB], item: Any) -> RB: ...

def parse_object(response_type, item):
    if issubclass(response_type, Parsable):
        return response_type.parse(item)
    return response_type(item)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
false-positive mypy gave an error on correct code feature priority-2-low topic-type-narrowing Conditional type narrowing / binder topic-type-variables
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants