Skip to content

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

Closed

Description

@antonagestam

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)

Activity

  1. JukkaL commented on Mar 20, 2020

    @JukkaL
    Collaborator

    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.

  2. antonagestam commented on Mar 20, 2020

    @antonagestam
    ContributorAuthor

    @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

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions