-
Notifications
You must be signed in to change notification settings - Fork 764
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
Cannot assign to variables of generic type #4112
Comments
Pyright (the type checker upon which pylance is built) is correct here. You cannot assign a value of type This appears to be a bug in mypy. Your code sample is defining a constrained TypeVar with types If you modify your sample to use constraints that are disjoint, such as T = TypeVar("T", int, str)
def f(arg: T):
arg = 1 # Type error You mentioned in your bug report that your type variable is "bound to" type T = TypeVar("T", bound=int) |
Hey thanks for the quick reply!
Sorry this was confusion from my side, I started learning python two days ago and went through a lot of new terminology. Anyway the error in case of floatNumber: float = 1 Why would it complain about the same thing written with generics rather than explicit types? Edit: I just realised that in the OP I never mentioned anything about assigning an Re-edit: FWIW the reason why in the OP I only spoke about assigning an from typing import TypeVar
T = TypeVar("T", int, float, str)
def fun(arg: T):
if isinstance(arg, int):
arg = 1 And when it didn't work I though it was because |
@debonte Thank you for looking into the issue. Is there any chance that you are going to implement the behaviour I was expecting? Is this a limitation of of pyright's design or just a feature no one ever needed? |
I saw that pyright is written in TypeScript, which allows for the behaviour I was expecting. Anyway thank you again for having looked into the issue! |
I am not sure when TypeVar has constraints, type var is considered as invariant or not. if it is not, then def fun(arg: T):
if isinstance(arg, int):
arg = 1 arg could be sub type of https://docs.python.org/3/library/typing.html#typing.TypeVar says TypeVar is by default invariant, but one of example says this
which says T will ever be resolved to I tried this from typing import List, TypeVar
T = TypeVar("T", Base, UnusedType)
derived = DerivedBase(1)
base = Base(1)
def funcWithTypeVar(arg: List[T]) -> T:
arg.append(Base(1)) # error - Argument of type \"Base\" cannot be assigned to parameter \"T@funcWithTypeVar\" in function \"append\"
return arg[0]
def funcWithRealType(arg: list[Base]):
arg.append(Base(1))
pass
arr: list[DerivedBase] = [derived, derived]
funcWithTypeVar(arr) # no error
funcWithRealType(arr) # error - \"list[DerivedBase]\" is incompatible with \"list[Base]\"\n TypeVar \"_T@list\" is invariant\n \"DerivedBase\" is incompatible with \"Base\"" where funcWithRealType shows errors saying _T@list is invariant but funcWithTypeVar accepted it fine. if T used for funcWithTypeVar is covariant, then |
@heejaechang Thank you for trying to shed light on the behaviour of pyright. I haven't fully understood covariance and contravariance (in particular the examples in here), but regarding the example you mentioned
I assume that for "T" you mean the generic type "A" that was defined as b = concatenate(StringSubclass('one'), StringSubclass('two'))
reveal_type(b) # revealed type is str, despite StringSubclass being passed in you say that T is acting as if it was covariant because a subtype of it is allowed to be used in its place. With that said, I don't understand why this should not work: from typing import TypeVar
T = TypeVar("T", int, float)
def f(arg: T):
arg = 1 As far as T is concerned, But as I said at the beginning of this comment, I haven't fully understood covariance (and generic types in general) yet, so I can't really defend my point of view, only put it out there to be considered by someone more knowledgeable. |
Pyright is doing the right thing here. This isn't a bug, so there's nothing to fix. You mentioned that this works in TypeScript, but TypeScript (and no other language, to my knowledge) has the notion of a "constrained TypeVar" like Python does. It was added mainly to handle the AnyStr type. I recommend against using it in general because it does not work the way most people expect, and there's almost always a better solution. As I said above, you're using a constrained TypeVar in an especially odd manner because you're specifying two constraints where one is a subtype of the other. I'm still not exactly sure what you're trying to do here, but I suspect what you really mean is for |
@DaviDevMod the reason I wasn't sure whether
I expected but def funcWithTypeVar(arg: List[T]) -> T:
arg.append(Base(1)) # this should be error
return arg[0]
arr: list[DerivedBase] = [derived, derived]
funcWithTypeVar(arr) # no error
unfortunately, currently For your example, if I have something like this class MyInt(int):
pass
T = TypeVar("T", int, float)
def f(arg: T):
arg = 1
f(MyInt(1)) and if def f(arg: MyInt):
arg = 1
you could say but if |
@heejaechang, variance applies only when a TypeVar is used as a type parameter for a class. In this case, |
@erictraut thank you for the explanation, so in "naked" type variable, how does it supposed to behave? like |
I'm not sure what you're referring to when you say "how does it supposed to behave"? The current behavior of pyright for this code snippet is correct. Constrained TypeVars involves some strange rules. I won't repeat all of those details here. You can look it up if you want. However, I recommend avoiding the use of constrained TypeVars if you are not an expert at Python typing. They're rarely the right tool for the job. Once I understand what the OP is trying to do, I can offer better advice about the best approach. |
I have been searching the rule but I couldn't find one. are you talking about this or this? or pyright's implementation? or some other docs? I will appreciate any pointer so I can learn about python generic a bit more. what I mean by I couldn't find one is explanation on why the code above show errors (so we can understand why it does what it does) arg.append(Base(1)) # error - Argument of type \"Base\" cannot be assigned to parameter \"T@funcWithTypeVar\" in function \"append\"
# or this
def f(arg: T):
arg = 1 by the way, it would be nice if like some other language |
@erictraut what I was trying to do was having a function that does the task This is off topicHere is an example of my problem: from typing import Literal
Answers = Literal["YES", "NO"]
Vowels = Literal["A", "E", "I", "O", "U"]
def fun(arg: Literal["INT", "ANSWERS", "VOWELS"]) -> int | Answers | Vowels:
print("Print this, no matter what `arg` is.")
if arg == "INT" and not input("") == 0:
return 5
elif arg == "ANSWERS" and input("") == "YES":
return "YES"
elif arg == "VOWELS" and input("") == "A":
return "A"
print("Invalid input.")
return fun(arg)
# Obviously these variables are all of the same type `int | Answers | Vowels`
# which is not what I want.
integer = fun("INT")
answer = fun("ANSWERS")
vowel = fun("VOWELS") What I did was using overloads, but the four overloads I need in my actual code are bigger than the function itself. Here is the above function overloaded: from typing import Literal, overload
Answers = Literal["YES", "NO"]
Vowels = Literal["A", "E", "I", "O", "U"]
@overload
def fun(arg: Literal["INT"]) -> int:
...
@overload
def fun(arg: Literal["ANSWERS"]) -> Answers:
...
@overload
def fun(arg: Literal["VOWELS"]) -> Vowels:
...
def fun(arg: Literal["INT", "ANSWERS", "VOWELS"]):
print("Print this, no matter what `arg` is.")
if arg == "INT" and not input("") == 0:
return 5
elif arg == "ANSWERS" and input("") == "YES":
return "YES"
elif arg == "VOWELS" and input("") == "A":
return "A"
print("Invalid input.")
return fun(arg)
integer = fun("INT")
answer = fun("ANSWERS")
vowel = fun("VOWELS") Another option would be singledispatch but it looks like it would be even more noisy. Using a generic function would be the obvious solution in case the type of the argument is the same as the return type. from typing import Literal, TypeVar
Answers = Literal["YES", "NO"]
Vowels = Literal["A", "E", "I", "O", "U"]
class AnswersProxy:
def __init__(self, answer: Answers = "YES"):
self.value: Answers = answer
class VowelsProxy:
def __init__(self, answer: Vowels = "A"):
self.value: Vowels = answer
T = TypeVar("T", int, AnswersProxy, VowelsProxy)
def fun(arg: T) -> T:
print("Print this, no matter what `arg` is.")
if isinstance(arg, int) and not input("") == 0:
return 5
elif isinstance(arg, AnswersProxy) and input("") == "YES":
return AnswersProxy("YES")
elif isinstance(arg, VowelsProxy) and input("") == "A":
return VowelsProxy("A")
print("Invalid input.")
return fun(arg)
integer = fun(int())
answer = fun(AnswersProxy()).value
vowel = fun(VowelsProxy()).value Which is definitely less noisy in my actual code (where each of Now I don't exactly know how the behaviour exposed in the OP would help create such a function. It is still not clear to me why the behaviour is expected by pyright. from typing import TypeVar
T = TypeVar("T", int, str)
def fun(arg: T):
if isinstance(arg, int):
arg = 1 Would you please point out why an error is expected here? |
Based on the problem you're describing, the correct tool for the job is an @overload
def fun(arg: int) -> float: ...
@overload
def fun(arg: str) -> bytes: ...
def fun(arg: int | str) -> float | bytes:
if isinstance(arg, int):
return float(arg)
return arg.encode() |
@erictraut Thank you so much for settling my inner battle, I'll accept the noise that overloads carry with them. But I still don't understand why an error is expected in here: from typing import TypeVar
T = TypeVar("T", int, str)
def fun(arg: T):
if isinstance(arg, int):
arg = 1 The docs say:
So within the scope of What am I missing here? |
Environment data
Code Snippet
Expected behavior
It should be possible to assing an
int
to a variable of a generic type bound toint
.mypy Playground runs without errors.
Actual behavior
When trying to assign an
int
to a variable of a generic type bound toint
, I get a squiggling line under the integer which expands to:Logs
Python Language Server Log
The text was updated successfully, but these errors were encountered: