python / mypy

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

Implementation of annotated abstractmethod is typed as Any #15669

Open anentropic opened 1 year ago

anentropic commented 1 year ago

Bug Report

If you add type annotations to an @abstractmethod in an ABC then I would expect that implementations of that method in concrete classes would be type-checked to match the types on the abstract method.

But mypy treats the implemented method as Any -> Any

To Reproduce

I was originally doing this with generics in the base class:

from __future__ import annotations

from abc import ABCMeta, abstractmethod
from datetime import date
from typing import Generic, TypeVar

T = TypeVar("T")

class GenericValidatorABC(Generic[T], metaclass=ABCMeta):
    @abstractmethod
    def validate(self, context, value: T) -> T:
        ...

class DateOfBirthValidator(GenericValidatorABC[date]):
    name = "date_of_birth"

    def validate(self, context, value):
        return value.year > 1900

reveal_type(DateOfBirthValidator.validate)

But it turns out that the problem also occurs in the simpler non-generic case too:

class DateValidatorABC(metaclass=ABCMeta):
    @abstractmethod
    def validate(self, context, value: date) -> date:
        ...

class DateOfBirthValidator2(DateValidatorABC):
    name = "date_of_birth"

    def validate(self, context, value):
        return value.year > 1900

reveal_type(DateOfBirthValidator2.validate)

Expected Behavior

I would expect mypy to infer, and require, the type of the implemented method to match the annotated type of the abstract method.

So there should be a type error from our implementations above as value is date but the method returns bool.

Actual Behavior

Instead, reveal_type shows:

main.py:21: note: Revealed type is "def (self: __main__.DateOfBirthValidator, context: Any, value: Any) -> Any"
main.py:37: note: Revealed type is "def (self: __main__.DateOfBirthValidator2, context: Any, value: Any) -> Any"

https://mypy-play.net/?mypy=latest&python=3.11&gist=9fc2dca490527a088fe7bc81601f1666

Your Environment

anentropic commented 1 year ago

A case that does cause mypy to complain is if the concrete implementation is fully annotated:

class DateValidatorABC(metaclass=ABCMeta):
    @abstractmethod
    def validate(self, context, value: date) -> date:
        ...

class DateOfBirthValidator3(DateValidatorABC):
    name = "date_of_birth"

    def validate(self, context, value: date) -> bool:
        return value.year > 1900

this gives:

error: Return type "bool" of "validate" incompatible with return type "date" in supertype "DateValidatorABC"  [override]

...which is exactly what I was hoping for.

I'm just using https://mypy-play.net/ with default options (I assume equivalent to mypy with no cli args), I wondered now if there was an option that would affect this.

Enabling --check-untyped-defs does not help the original examples.

--disallow-untyped-defs does of course error for the untyped method def, but not the incompatible type.

erictraut commented 1 year ago

Pyright implements type inference for parameters in subclasses based on annotations in parent classes, so it is consistent with the "expected behavior" you've requested above.

Mypy maintainers have previously stated that they think parameter types should always be explicit, even in subclasses. I don't know if their position has changed since then. Perhaps one of them could clarify.

anentropic commented 1 year ago

@erictraut thanks

I am also in favour of explicit parameter types, but it seems weird to allow a (possibly unintended) implicit Any in subclass to override an explicit typing in the parent 🤔

seems like mypy needs to be run with --strict option or results are likely to be worthless