pytype icon indicating copy to clipboard operation
pytype copied to clipboard

Mixin resolved as Any causes false positive not-instantiable

Open cebtenzzre opened this issue 2 years ago • 3 comments

Example

a.py

import abc

from b import Mixin  # pytype: disable=import-error

class Base(abc.ABC):
    @abc.abstractmethod
    def foo(self):
        raise NotImplementedError

class Derived(Mixin, Base):
    pass

x = Derived()
print(x.foo())

b.py

class Mixin:
    def foo(self):
        return 'Mixin foo'

Expected Behavior

I run pytype-single a.py, simulating a third-party import that pytype cannot resolve to a specific type (so Mixin is typed as Any). There should be no error when Derived is instantiated. I expect this because pytype "allows all operations that succeed at runtime and don't contradict annotations".

Actual Behavior

$ python a.py
Mixin foo
$ pytype-single a.py
File "a.py", line 13, in <module>: Can't instantiate Derived with abstract methods foo [not-instantiable]

For more details, see https://google.github.io/pytype/errors.html#not-instantiable

This is the generated .pyi:

import abc
from typing import Any

Mixin: Any
x: Derived

class Base(abc.ABC):
    @abstractmethod
    def foo(self) -> Any: ...

class Derived(Any, Base): ...

There is no error if the body of Base is replaced with "pass", so pytype appears to recognize that it cannot know whether foo is implemented when no abstract methods are involved.

Software Versions

pytype 2022.03.08 Python 3.9.9

cebtenzzre avatar Mar 18 '22 18:03 cebtenzzre

I'm a little conflicted on this one. On one hand, it's true that Mixin may implement Base.foo, so reporting not-instantiable is perhaps overly strict. On the other hand, a class sometimes has Any in its MRO for unrelated reasons, and pytype suddenly failing to detect not-instantiable errors because an unrelated Any crept in also seems like somewhat unfriendly behavior.

There is no error if the body of Base is replaced with "pass", so pytype appears to recognize that it cannot know whether foo is implemented when no abstract methods are involved.

FYI the reason pytype allows this is that Python itself allows abstract base classes to be instantiated if they have no abstract methods.

rchen152 avatar Mar 21 '22 23:03 rchen152

@rchen152 My point about the contents of Base making a difference is that pytype already gives the program the benefit of the doubt for attribute-error, because Any is in MRO, but not for not-instantiable, despite Any coming before the abstract class in the MRO. pytype brands itself as lenient, but it is currently stricter than mypy in this case - even with --strict, mypy will complain about subclassing Any but won't report "Cannot instantiate abstract class".

cebtenzzre avatar Mar 22 '22 01:03 cebtenzzre

My point about the contents of Base making a difference is that pytype already gives the program the benefit of the doubt for attribute-error, because Any is in MRO, but not for not-instantiable, despite Any coming before the abstract class in the MRO.

Ah ok, I see your point. I'm still a little iffy on this, because we do sometimes get bug reports about the attribute-error behavior, too. The problem is that when the Any in the MRO is unexpected, it's an unpleasant surprise when obvious errors are missed.

pytype brands itself as lenient, but it is currently stricter than mypy in this case - even with --strict, mypy will complain about subclassing Any but won't report "Cannot instantiate abstract class".

I don't want to spend too much time on this, since I don't think it'll be a productive avenue of discussion, but since it's come up a second time: I'm perfectly aware of the contents of our README without it being quoted to me. It's an explanation of pytype's general philosophy, not a legal contract.

rchen152 avatar Mar 22 '22 02:03 rchen152