Skip to content

Type narrowing behavior for data descriptor assignment is incorrect in some cases #10399

Closed
@erictraut

Description

@erictraut

Mypy normally performs type narrowing for assignment statements when the LHS is a member access (e.g. a.b = x). However, it appears to avoid type narrowing in certain circumstances when b is a descriptor. This special casing leads to incorrect behaviors in some cases. (Look for "!!!" in the sample below.)

from typing import Any, Protocol, Union

class HasName(Protocol):
    def __name__(self) -> str:
        ...

class Animal:
    def __name__(self) -> str:
        ...

class Dog(Animal):
    def __name__(self) -> str:
        ...

    def run(self) -> None:
        print("Run Spot Run!")

    def bark(self) -> None:
        print("Ruff!")

class Cat:
    def __name__(self) -> str:
        ...

    def run(self) -> None:
        print("Run Kitty Run!")

    def purr(self) -> None:
        print("Purr!")

class AnimalMember:
    def __get__(self, instance: Any, owner: Any) -> Animal:
        ...

    def __set__(self, instance: Any, value: HasName) -> None:
        ...

class Struct:
    member = AnimalMember()

def test_descriptor(cat: Cat, dog: Dog, cat_or_dog: Union[Cat, Dog]):
    inst = Struct()

    inst.member = cat_or_dog
    reveal_type(inst.member)  # mypy: Revealed type is 'Animal'
    inst.member.run()  # mypy: "Animal" has no attribute "run"
    inst.member.bark()  # mypy: "Animal" has no attribute "bark"
    inst.member.purr()  # mypy: "Animal" has no attribute "purr"

    inst.member = cat
    reveal_type(inst.member)  # mypy: Revealed type is 'Animal'
    inst.member.run()  # mypy: "Animal" has no attribute "run"
    inst.member.bark()  # mypy: "Animal" has no attribute "bark"
    inst.member.purr()  # mypy: "Animal" has no attribute "purr"

    inst.member = dog
    reveal_type(inst.member)  # mypy: Revealed type is 'Dog'
    inst.member.run()  # mypy: no error
    inst.member.bark()  # mypy: no error
    inst.member.purr()  # mypy: "Dog" has no attribute "purr"

    inst.member = cat_or_dog
    reveal_type(inst.member)  # mypy: Revealed type is 'Dog' !!!!
    inst.member.run()  # mypy: no error
    inst.member.bark()  # mypy: no error
    inst.member.purr()  # mypy: "Dog" has no attribute "purr"

    inst.member = cat
    reveal_type(inst.member)  # mypy: Revealed type is 'Dog' !!!!
    inst.member.run()  # mypy: no error
    inst.member.bark()  # mypy: no error !!!!
    inst.member.purr()  # mypy: "Dog" has no attribute "purr" !!!!

Refer to this bug report in the pyright repo for details and a discussion of opitons. I'm interested in thoughts on a more robust heuristic that we can use across type checkers to handle this case properly.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugmypy got something wrongtopic-descriptorsProperties, class vs. instance attributestopic-type-narrowingConditional type narrowing / binder

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions