python / mypy

Optional static typing for Python
https://www.mypy-lang.org/
Other
18.24k stars 2.79k forks source link

Feature Request: Infer TypeVar Bounds from assert issubclass Statements #17676

Open mbellgardt opened 1 month ago

mbellgardt commented 1 month ago

Feature

I would like to request an enhancement in mypy to allow it to infer TypeVar bounds from assert issubclass(...) statements. Currently, mypy uses such assertions for local type refinement within the scope of the assertion, but it does not propagate these refinements globally, particularly for type variables. This leads to cases where developers must manually specify TypeVar bounds, even though the bound could be logically inferred from the assertions in the code.

Use Case:

Consider a scenario where a function takes a Type[T] parameter, and an assert issubclass(cls, MyAbstractClass) check is used to ensure that cls is a subclass of MyAbstractClass. Despite this assertion, mypy does not infer that T is bound by MyAbstractClass, which can lead to type-checking issues where mypy cannot guarantee that cls has the necessary methods or attributes.

Example:

from typing import Type, TypeVar, BinaryIO
from abc import ABC, abstractmethod

class MyAbstractClass(ABC):
    @classmethod
    @abstractmethod
    def from_binary_io(cls, data: BinaryIO) -> 'MyAbstractClass':
        pass

T = TypeVar('T')

def create_instance(cls: Type[T], data: BinaryIO) -> T:
    assert issubclass(cls, MyAbstractClass)
    return cls.from_binary_io(data)

# Example subclass implementation
class MyConcreteClass(MyAbstractClass):
    def __init__(self, content: bytes):
        self.content = content

    @classmethod
    def from_binary_io(cls, data: BinaryIO) -> 'MyConcreteClass':
        content = data.read()
        return cls(content)

In this example, mypy does not infer that T is bound by MyAbstractClass, leading to type-checking issues when calling cls.from_binary_io(data).

Proposed Enhancement:

mypy should be able to infer the bound of T as MyAbstractClass based on the assert issubclass(cls, MyAbstractClass) statement. This enhancement would allow developers to avoid manually specifying bounds for TypeVar when they can be logically deduced from the code.

Pitch

Benefits:

Potential Challenges:

I believe this feature would significantly improve mypy's type inference capabilities and make it even more powerful for developers working with generics and abstract base classes.

erictraut commented 1 month ago

This is probably just a terminology issue, but I don't think you're asking for better "inference" behavior or anything regarding the (upper) bound of the TypeVar. Instead, I think you're asking for better type narrowing behavior for the issubclass call.

The assert issubclass(cls, MyAbstractClass) statement should (from a type theory perspective) narrow the type of cls from type[T] to type[T & MyAbstractClass], where & represents an "intersection". Intersections are not currently part of the Python type system, but mypy does generate intersections internally in some cases for isinstance and issubclass narrowing. It could theoretically do the same in this case. This is the technique I implemented in pyright, so there is a proof point that this approach works.

There have been some recent discussions about formally adding intersections to the Python type system. If that happens, there will be a straightforward way to handle this case.