mypy icon indicating copy to clipboard operation
mypy copied to clipboard

a Type is not recognized properly for subclassing

Open hydrargyrum opened this issue 4 years ago • 9 comments

Bug Report

I'm explicitly typing a variable with Type (or type) and mypy thinks I should not subclass it.

To Reproduce

from typing import Type

class Base:
    ...

def get_base() -> Type[Base]:
    return Base

BaseAlias: Type[Base] = get_base()

class Derived(BaseAlias):
    ...

Expected Behavior

There should be no error. I'm subclassing a type, which is ok. Furthermore, mypy has the information that it's a type so subclassing should not be questioned.

Actual Behavior

footypes.py:11: error: Variable "footypes.BaseAlias" is not valid as a type
footypes.py:11: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases
footypes.py:11: error: Invalid base class "BaseAlias"
Found 2 errors in 1 file (checked 1 source file)

Same behavior if specifying BaseAlias: type = get_base() instead.

Your Environment

  • Mypy version used: 0.910
  • Mypy command-line flags: none
  • Mypy configuration options from mypy.ini (and other config files): none
  • Python version used: 3.9.0
  • Operating system and version: python:3.9 docker image

hydrargyrum avatar Aug 10 '21 11:08 hydrargyrum

Even this does not work:

class Base:
    ...

def get_base() -> Base:
    ...

BaseAlias = get_base()

class Derived(BaseAlias):
    ...

Outputs:

ex.py:11: error: Variable "ex.BaseAlias" is not valid as a type
ex.py:11: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases
ex.py:11: error: Invalid base class "BaseAlias"

I'll see what we can do here.

sobolevn avatar Sep 30 '21 08:09 sobolevn

I guess that the easiest way to explain this is with this example:

from typing import Type

class Base:
    ...

class NotSoBase(Base):
    a = 1

def get_base() -> Type[Base]:
    return NotSoBase

BaseAlias: Type[Base] = get_base()

class Derived(BaseAlias):
    ...

This example will raise the same error. But, it differs in what type we return from get_base(). Notice, that -> Type[Base] return annotation is still repsected, but we return NotSoBase type instead, which has extra fields.

In your example you expect Mypy to understand that we use Base as a supertype, but my counter-example shows that this is not trivial. We can end up with quite broken TypeInfos this way. 😞

sobolevn avatar Sep 30 '21 09:09 sobolevn

@sobolevn Would you mind explaining you couterexample a bit more? As far as I understand, using BaseAlias as a superclass should basically only say "I'm defining something that implements the interface of Base", because the alias has type Type[Base]. Even if you have another superclass between them in the chain (NotSoBase in your example), the substitution principle shouldn't be violated.

I'm asking because I think I have another example which is related to this issue: https://mypy-play.net/?mypy=latest&python=3.10&gist=4f4e45da223f0991300d6a71bb14724d

yrd avatar Dec 02 '21 08:12 yrd

@yrd I think that this:

def get_base() -> Type[Base]:
    return NotSoBase

BaseAlias: Type[Base] = get_base()

class Some(BaseAlias): ...

Will generate inconsistent MRO for mypy. Imagine that this code is valid. Then, mypy would think that Some is a subtype of Base, but in reallity it would be a subtype of NotSoBase.

sobolevn avatar Dec 02 '21 12:12 sobolevn

Hm, I see that now - thanks!

yrd avatar Dec 06 '21 06:12 yrd

I've been hitting what I think is this same issue in bidict too. I've been silently 'type: ignoring' it, but figured I'd chime in here, in case this is another useful test case:

def namedbidict(
    typename: str,
    keyname: str,
    valname: str,
    *,
    base_type: type[BidictBase[KT, VT]] = bidict,
) -> type[BidictBase[KT, VT]]:
    ...
    class NamedBidict(base_type):  # false positive errors here (see below)
        ...
$ mypy bidict
bidict/_named.py: note: In function "namedbidict":
bidict/_named.py:72:23: error: Variable "base_type" is not valid as a type
[valid-type]
        class NamedBidict(base_type):
                          ^
bidict/_named.py:72:23: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases
bidict/_named.py:72:23: error: Invalid base class "base_type"  [misc]
        class NamedBidict(base_type):
                          ^
Found 2 errors in 1 file (checked 26 source files)

jab avatar Oct 01 '23 17:10 jab

You can model namedbidict as a class :)

sobolevn avatar Oct 01 '23 17:10 sobolevn

If I understand you correctly, the namedbidict API has been in bidict since 2009, long before Python had type hints. Sharing this example in case it’s desirable for mypy to support such APIs. If not, fair enough!

jab avatar Oct 01 '23 23:10 jab

This affects setuptool's get_unpatched method, which is typed as such:

def get_unpatched(item: _T) -> _T: ...

# Used like:
_Distribution = get_unpatched(distutils.core.Distribution)

It also lead to unexpected invalid-type errors for downstream users of libraries not aware of this problem. For instance, platformdirs.PlatformDirs is currently an invalid type: https://github.com/jaraco/jaraco.abode/blob/8843f360ee5d9bc1afb1bdb3119157addb4aaf06/jaraco/abode/config.py#L4C7-L4C19

Avasam avatar Aug 22 '24 03:08 Avasam