-
-
Notifications
You must be signed in to change notification settings - Fork 3k
Cannot infer return type if using Union[Tuple[Literal[]]...] #7399
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
Comments
I can reproduce the issue. Apparently the literal type context is sometimes lost. Some observations:
cc @Michael0x2a |
I think this is a deeper issue than just affecting Take for example: from typing import Union, List, Tuple
a: List[bool] = [True, True, False]
b: Union[List[Union[bool, int]], Tuple[Union[bool, int], ...]] = a
print(b) Which outputs:
(They are compatible: Take the Interesting this works when using from typing import Union, List, Tuple
a: Tuple[bool, ...] = (True, True, False)
b: Union[List[Union[bool, int]], Tuple[Union[bool, int], ...]] = a
print(b) Which outputs:
Or, a simpler reproducer: from typing import Union, List
a: List[bool] = [True, True, False]
b: List[Union[bool, int]] = a
print(b) Which outputs:
I think invariance here is a red herring. Using Note that the base types obviously work: from typing import Union
a: bool = True
b: Union[bool, int] = a
print(b) Which outputs:
My use case is padding of arrays. The padding might be of a fixed type (say, from typing import Union, List
class MyClass:
pass
def pad(data: List[Union[bool, int, MyClass]], width):
amount = width - len(data)
padding = [False] * amount
return padding + data
print(pad([1, 2, 3], 5)) Which outputs:
At any rate, that's my 2c. Hope it helps. |
What you describe is a consequence of List being invariant. See https://mypy.readthedocs.io/en/latest/common_issues.html#invariance-vs-covariance. |
I guess I don't understand how or perhaps, that I feel it is too strict. :) (Do tell me where I'm wrong in the below -- perhaps I just need to accept that assignment across explicit, compatible types can be called variance). My understanding is that variance mostly relates to a class hierarchy (which I guess In the Generic types section, we're given a class Shape:
pass
class Circle(Shape):
def rotate(self):
... I do completely agree that, given a variable annotated as class Shape:
def area(self):
...
class Circle(Shape):
def rotate(self):
...
def sum_area(shapes: List[Shape]):
return sum(map(lambda x: x.area(), shapes))
l: List[Circle] = [Circle(), Circle(), Circle()]
print(sum_area(l)) We get the same (IMO red herring) message about covariance:
But my understanding is, since we're going from a derived type to its parent type ( This is demonstrated by changing
I guess we could all call this covariance (though, assigning across explicit types seems like it should avoid covariance related discussions, unlike implicit typing where available information at the time of assignment makes type inference hard). But to me it seems worthwhile to then notate that one type of covariance is easy to handle (removing features by going up the parent -> child class hierarchy) and the other is hard (adding new features by going down the parent -> child class hierarchy). The former definitely seems like something mypy could (with time and code...:) learn to handle correctly; the latter I agree should be a type error with invariant types (and more leniently handled with covariant types). I don't see the value in an explicit call to list (which solves the first example's covariance warning) when both halves are explicitly annotated. It seems like an unannotated copy lets it act as an unnecessary gluing mechanism between two explicit, compatible types. The following typechecks though, so I guess I'll live with lacking the explicit type annotation on from typing import Union, List, Tuple, Sequence, Iterable, Optional
class Variable:
pass
VariableLike = Union[bool, int]
VariableActually = Union[VariableLike, Variable]
class Vector:
items: Tuple[VariableActually, ...]
def __init__(self, items: Iterable[VariableActually]):
self.items = tuple(items)
VectorLike = Union[Tuple[VariableActually, ...], List[VariableActually], int]
VectorActually = Union[VectorLike, Vector]
def to_list(obj: int): # NB: can't be annotated as List[bool] -- otherwise to_vector fails below
# Lazy implementation.
return [False, False, False, obj % 2 == 0]
def to_vector(other: VectorActually, width: Optional[int] = None):
if isinstance(other, Vector):
return to_vector(other.items, width=width)
# From this point on, mypy does detect that the remaining
# types out of VectorActually for other are only those in
# VectorLike.
if isinstance(other, int):
obj = to_list(other)
return to_vector(obj, width=width)
# From this point on, mypy does detect that the remaining
# types out of VectorActually (now VectorLike) are the List/Tuple
# of VariableActually's.
if width:
l_other = len(other)
if l_other > width:
raise ValueError("...")
padding: List[VariableActually] = [False] * (width - l_other)
return Vector(padding + list(other))
return Vector(other) |
I ran into this issue/issues similar to this while working on some literal types stuff, but don't quite have time to fix this now. So, here are some notes so I don't lose context once I circle back around to this... I think for the original example, the root cause comes from this: https://github.com/python/mypy/blob/master/mypy/checkexpr.py#L3067 In short, if we have a union of tuples, we currently just completely give up on attempting to select a literal context at all. We could maybe do together by "splicing" together a union of tuples -- e.g. if we have I'm a little surprised we don't already have logic for doing this sort of thing, actually. While doing some more testing, I also ran into the following similar example:
In this case, I'm pretty sure the issue stems from how we attempt to figure out if we should be using a literal context in https://github.com/python/mypy/blob/master/mypy/checkexpr.py#L242 -- we don't attempt to destructure the type and instead enable the literal context only if the variable corresponds to an instance. We actually have a test case that I think fails for this reason, actually -- One quick-and-dirty (?) way of fixing this specific issue would be to modify This would also incidentally fix the first example, but only as a side-effect and for literal types only. This tactic in general is unsafe if we've accidentally forgotten to erase the last known value field somewhere though, so I'm hesitant to use it. I'm pretty sure the issue @cipherboy reported is unrelated though, and really is a case of mypy correctly reporting an issue with variance. For example, attempting to pass in a
|
Note that tuple of two unions is a union of four tuples in general. But yes, mypy is not very good at union math and it is indeed special-cased, so we have things like |
I just stumbled across a similar issue, but w/o nested The code below the minimal example of a plugin system where plugin classes are added to a register (dict). from typing import MutableMapping, Tuple, Union
class A:
pass
class B:
pass
register_a: MutableMapping[str, A] = {}
register_b: MutableMapping[str, B] = {}
def add_to_register(
key: str,
item_register_tuple: Union[Tuple[A, MutableMapping[str, A]], Tuple[B, MutableMapping[str, B]]],
):
item, register = item_register_tuple
register[key] = item $ mypy --version
mypy 0.812
$ mypy --show-error-codes --ignore-missing-imports dummy.py
dummy.py:21: error: Incompatible types in assignment (expression has type "Union[A, B]", target has type "A") [assignment]
dummy.py:21: error: Incompatible types in assignment (expression has type "Union[A, B]", target has type "B") [assignment] The way I see it Please correct me if there is an error in my interpretation. |
I'm seeing a similar error here: from typing import *
TElement = Union[Tuple[Literal["int"], int], Tuple[Literal["str"], str]]
xs: List[TElement] = []
xs.append(("int", 1))
xs.append(("str", "what")) and mypy interprets the |
The underlying issue is now fixed in mypy, see python/mypy#7399
The underlying issue is now fixed in mypy, see python/mypy#7399
or a mock-up repro if the source is private. We would appreciate
if you try to simplify your case to a minimal repro.
What is the actual behavior/output?
It fails to recognize that
(1,)
is aTuple[Literal[1]]
What is the behavior/output you expect?
What are the versions of mypy and Python you are using?
Do you see the same issue after installing mypy from Git master? yes
What are the mypy flags you are using? (For example --strict-optional)
I'm not sure if this is expected behaviour, or a limitation, but I thought I should point it out since I couldn't find any existing bugs for this.
The text was updated successfully, but these errors were encountered: