Skip to content

Generic[T_co] erroring when T_co = TypeVar("T_co", A, B, covariant=True) and passed subclass of A #8806

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
jamesbraza opened this issue May 12, 2020 · 9 comments

Comments

@jamesbraza
Copy link
Contributor

jamesbraza commented May 12, 2020

I am reporting what I believe is a bug in mypy's interpretation of TypeVar when passed two constraining types and also covariant=True.

I think the below code sample is valid (per Liskov Substitution Principle), and is erroneously being reported as a type error by mypy. Here is my working example:

from typing import TypeVar, Generic, Union

class A:
    pass

class ASub(A):
    pass

class B:
    pass

# Case 1... Success: no issues found
# T_co = TypeVar("T_co", covariant=True)

# Case 2... error: Value of type variable "T_co" of "SomeGeneric" cannot be "ASub"  [type-var]
T_co = TypeVar("T_co", A, B, covariant=True)

# Case 3... Success: no issues found
# T_co = TypeVar("T_co", bound=Union[A, B], covariant=True)


class SomeGeneric(Generic[T_co]):
    pass

class SomeGenericASub(SomeGeneric[ASub]):
    pass

Potentially Related Issue

I may be re-asking the contents of #8611, but to be honest, I get confused by the core of the issue

non-empty covariant generic subclass of a contravariant base


Versions

python==3.6.5
mypy==0.770
@JelleZijlstra
Copy link
Member

My understanding is that a typevar with value restriction (like Typevar("T", A, B) can only take the types A and B, not a subtype of either, so it doesn't really make sense to mark a typevar with value restriction as covariant.

If that's right, the correct fix would be for mypy to disallow having both a value restriction and covariant=True.

Have you tried TypeVar("T_co", bound=Union[A, B], covariant=True)?

@jamesbraza
Copy link
Contributor Author

jamesbraza commented May 13, 2020

Okay @JelleZijlstra I did not know that, and I don't see it stated in PEP 484. I guess this is why.

I updated the code snipped in the OP to include the case you mention, which indeed passes (and makes sense that it passes).

# Case 3... Success: no issues found
T_co = TypeVar("T_co", bound=Union[A, B], covariant=True)

Since TypeVar doesn't support supplying both value restrictions and covariant=True, I think it may be useful for mypy to raise a warning or error if this is done.

I would be happy to make a PR for this, if you think it's worthwhile. If yes, can you give me some pointers, and let me know if you think it's better as a warning or an error?

@JelleZijlstra
Copy link
Member

I think the process_typevar_parameters() method in mypy/semanal.py is the right place. Should be a pretty easy change.

But I'd wait for somebody more knowledgeable in the theory behind typevars (maybe @ilevkivskyi?) to confirm that TypeVar("T_co", A, B, covariant=True) doesn't make sense.

@ilevkivskyi
Copy link
Member

I think something like TypeVar("T_co", int, float, covariant=True) still makes sense, although pretty exotic. Variance is not a property of type variable, it is just an unfortunate syntax, variance is a property of a class. This would just indicate that SomeGeneric[int] <: SomeGeneric[float].

@jamesbraza
Copy link
Contributor Author

jamesbraza commented May 13, 2020

In addition to @ilevkivskyi 's post, looking at the OP's linked issue (#8611), they also supply both value restrictions and covariant=True. So I do think it's valid to supply TypeVar with 2+ value restrictions and covariant=True.

What I am noticing is that (in both Ivan's example and #8611) the first value restriction is a subclass of the second. Is this how it's supposed to work? I tested those cases here.

# Case 4... Success: no issues found
T_co = TypeVar("T_co", A, ASub, covariant=True)

# Case 5... Success: no issues found
T_co = TypeVar("T_co", ASub, A, covariant=True)

However, both Case 4 and Case 5 just seem to boil down to:

# Case 6... Success: no issues found
T = TypeVar("T", bound=A)

In my opinion, supplying TypeVar with multiple value restrictions and covariant=True only makes sense when two un-related classes are used (as I have in the OP). Would you agree?

And to circle back to the original question, would you agree that this is a bug? I still think in the original question, that the code is valid, and mypy should not be raising an error.

@JelleZijlstra
Copy link
Member

Actually, it only makes sense when the classes are related (thanks Ivan for the example!). As I said before, a typevar with value restrictions can only take on the types given as value restrictions, not any subtype of these types.

Therefore, in Ivan's example (a covariant typevar restricted to int and float), SomeGeneric[int] is a subtype of SomeGeneric[float], but if the typevar had been invariant SomeGeneric[int] would not have been a subtype of SomeGeneric[float]. (Note that to mypy, int is a subtype of float.) If the types in your value restriction are unrelated, covariance will not make a difference.

@jamesbraza
Copy link
Contributor Author

I see where you're going with:

a typevar with value restrictions can only take on the types given as value restrictions, not any subtype of these types.

Also, thank you for mentioning mypy sees int as a subtype of float (documented here).


So then, I guess when supplying both value restrictions and covariant=True, the positional order of the value restrictions matters?

It seems like if the ordering had been TypeVar("T_co", float, int, covariant=True), that it would have implied SomeGeneric[float] <: SomeGeneric[int].


Also, to reference the above examples, can you please confirm my understanding?

T_co = TypeVar("T_co", ASub, A, covariant=True)  # Case 5

Case 5 means:

  • T_co is value restricted to ASub and A
  • T_co also "knows" SomeGeneric[ASub] <: SomeGeneric[A]

@JelleZijlstra
Copy link
Member

I don't think the ordering matters; what matters is that int <: float and therefore SomeGeneric[int] <: SomeGeneric[float] (if the typevar is covariant).

I agree with your understanding of case 5. Your second statement is true because ASub <: A.

@jamesbraza
Copy link
Contributor Author

jamesbraza commented May 13, 2020

Thank you for confirming.

So in both Ivan's example and my Case 5, it works because the value restrictions are related external to the T_co = TypeVar expression. (and not because of positional ordering of the value restrictions in the T_co = TypeVar expression).

So then that illuminates a potential mypy improvement!

My original question was built around a misunderstanding and this:

T_co = TypeVar("T_co", A, B, covariant=True)  # Case 2 (note: A and B aren't related)

Do you think it would be a useful feature for mypy to check that the value restrictions A and B are related when covariant=True? And if they're not related, then spit out a warning/error?

I am thinking an error message along the lines of covariant type variables requires value restrictions to be covariant

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants