-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
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
mypy does not recognize conversion between fixed length list and tuple #7509
Comments
This is a design limitation of mypy. Mypy currently doesn't even try to keep track of the lengths of list objects. However, issues similar to this come up every once in a while, and this is something mypy might support at some point in the future. You can work around this limitation by using a cast or a |
Is there an issue for |
We can use this issue to track both fixed-length lists and sequences for now. |
I would also appreciate this feature. My use-case is even simpler, converting fixed-length tuples to fixed-length tuples. Specifically I would like this to work: from typing import Tuple
t: Tuple[int, int] = (0, 0)
t = tuple(x for x in t) |
This comes up a lot when you're splitting strings: return dict(kv.split('=') for kv in ['a=b', 'cdf=ghi']) It might be nice to have a special split method on string like |
I have run into this as well and wanted to summarize my understanding of the three possible workarounds based on the comments from @JukkaL, @simsa-st, and @NeilGirdhar above. Rewrite code to avoid lists and generatorsA workaround that can be used even in the absence of
One caveat here is that the return value of Similarly when sorting or doubling every element in a tuple, you can do:
In this example In summary, neither lists nor generators have lengths in mypy, but it is often possible to rewrite code so that it avoids using lists or generators and uses only tuples (which can have lengths in mypy). Ignore the mypy error via comment or castAn alternative perspective is that sizing is "too much work" or "too unpythonic" to implement this way (we do love generator expressions after all), and so instead of the previous example, we can write (based on @JukkaL's suggestion):
or
These still gain the "benefits" of a sized return type, i. e.
still causes mypy to throw My preliminary understanding is that the main drawback of ignore/cast is that each one you write creates the possibility of false negatives occuring in the future whenever the surrounding code is modified. Accept the unsized tuple typeIf you don't care about catching the index out of range error, you can write:
which passes mypy but fails to catch the index out of range error in This workaround does not apply to @NeilGirdhar's use case, because |
@thomasgilgenast That's a good summary of what we can do right now, but we don't want to work around this problem. We want mypy to keep track of sequence lengths. We want: x: Sequence[int]
if len(x) == 2:
y: Tuple[int, int] = tuple(x) We want this to happen without any casting or ignoring or auxilliary functions. |
Yes, I found this thread after having attempted to simplify return {k: v for k, v in (var.split('=', maxsplit=1) for var in env)} into return dict(var.split('=', maxsplit=1) for var in env) and then saw the Mypy-complaints. So yet another workaround that passes at least Mypy 0.720 checks is to go via a dictionary comprehension with tuple unpacking like the original form above. That will instead trigger a Pylint warning:
so for Pylint users not much will be won in any case. |
This adds coverage for most of the public API, but there are still some areas that aren't covered yet. * script_host.py is ignored entirely * host.py is ignored entirely * the msgpack_rpc submodule has partial coverage * there are some Any annotations sprinkled around on some of the more complex functions Funtionality should remain largely unchanged. Notable exceptions are: * Buffer.mark() and Window.cursor now return a tuple instead of a list. This is because there is currently no type for a fixed-size list (python/mypy#7509)
### **SUMMARY:** It is unnecessary to perform `n + 1` calls to `cast` (one cast for each parameter name-value pair and one cast for the filter generator itself) in a dictionary comprehension in an effort to avoid mypy errors. Previously, the `cast` to `Tuple[str, str]` was necessary to prevent mypy from complaining that we are trying to create a dictionary out of lists as opposed to tuples (see the mypy issue [here](python/mypy#7509)). However, a `cast` is both adding unnecessary overhead due to the function call and should generally only be used when a linter is unable to properly infer the type of a variable, not to "lie" to it about the type. We can avoid this by instead using a generator within the dictionary comprehension and then indexing into it twice to produce tuples of size 2; mypy recognizes this as a valid dictionary initialization. The above change is much more performant than the previous version of the code. Timing the two versions of the dictionary construction yielded the following results: ``` >python -m timeit -s "from typing import cast, Dict, Tuple, Iterable" -n 100000 -p "dict(cast(Tuple[str, str], pair.split('=')) for pair in cast(Iterable[str], filter(None, 'rank=3&world_size=5'.split('&'))))" 100000 loops, best of 5: 2.66 usec per loop >python -m timeit -n 100000 -p "dict((pair[0], pair[1]) for pair in (pair.split('=') for pair in filter(None, 'rank=3&world_size=5'.split('&'))))" 100000 loops, best of 5: 1.09 usec per loop ``` The `cast` to `Iterable[str]` is similarly a "lie" that is not necessary. It is best to be as transparent as possible to the linter rather than using `cast` to eliminate errors. This actually does not even produce any mypy errors when removed in isolation from the other changes. Further, it is good practice to type hint the return value of a function rather than specifying the type of the return value inside the function. Thus, the unnecessary intermediate variable `query_dict` inside `_query_to_dict` was removed, and the type hint of the return value was moved to the function declaration. The type of the argument `query` is specified as `str`. ### **EDITS (additional commits):** [The sole type hint for `query_dict` (in `_env_rendezvous_handler`) was removed to match all other functions in the file.](76d78bf) [Incorrect typing is fixed for _env_rendezvous_handler typing so that `rank`, `world_size`, `master_port`, and `master_addr` are specified to be `int`, `int`, `int`, and `str`, respectively.](3cc5844) Pull Request resolved: #75959 Approved by: https://github.com/kumpera, https://github.com/mrshenli
) Summary: ### **SUMMARY:** It is unnecessary to perform `n + 1` calls to `cast` (one cast for each parameter name-value pair and one cast for the filter generator itself) in a dictionary comprehension in an effort to avoid mypy errors. Previously, the `cast` to `Tuple[str, str]` was necessary to prevent mypy from complaining that we are trying to create a dictionary out of lists as opposed to tuples (see the mypy issue [here](python/mypy#7509)). However, a `cast` is both adding unnecessary overhead due to the function call and should generally only be used when a linter is unable to properly infer the type of a variable, not to "lie" to it about the type. We can avoid this by instead using a generator within the dictionary comprehension and then indexing into it twice to produce tuples of size 2; mypy recognizes this as a valid dictionary initialization. The above change is much more performant than the previous version of the code. Timing the two versions of the dictionary construction yielded the following results: ``` >python -m timeit -s "from typing import cast, Dict, Tuple, Iterable" -n 100000 -p "dict(cast(Tuple[str, str], pair.split('=')) for pair in cast(Iterable[str], filter(None, 'rank=3&world_size=5'.split('&'))))" 100000 loops, best of 5: 2.66 usec per loop >python -m timeit -n 100000 -p "dict((pair[0], pair[1]) for pair in (pair.split('=') for pair in filter(None, 'rank=3&world_size=5'.split('&'))))" 100000 loops, best of 5: 1.09 usec per loop ``` The `cast` to `Iterable[str]` is similarly a "lie" that is not necessary. It is best to be as transparent as possible to the linter rather than using `cast` to eliminate errors. This actually does not even produce any mypy errors when removed in isolation from the other changes. Further, it is good practice to type hint the return value of a function rather than specifying the type of the return value inside the function. Thus, the unnecessary intermediate variable `query_dict` inside `_query_to_dict` was removed, and the type hint of the return value was moved to the function declaration. The type of the argument `query` is specified as `str`. ### **EDITS (additional commits):** [The sole type hint for `query_dict` (in `_env_rendezvous_handler`) was removed to match all other functions in the file.](76d78bf) [Incorrect typing is fixed for _env_rendezvous_handler typing so that `rank`, `world_size`, `master_port`, and `master_addr` are specified to be `int`, `int`, `int`, and `str`, respectively.](3cc5844) Pull Request resolved: #75959 Approved by: https://github.com/kumpera, https://github.com/mrshenli Test Plan: contbuild & OSS CI, see https://hud.pytorch.org/commit/pytorch/pytorch/d65ab9a689bc86fbdf589546dc47c69bee3bb971 Reviewed By: seemethere Differential Revision: D35902000 Pulled By: seemethere fbshipit-source-id: 0f7b357e4aca35945eef576ee13e047982de1e9e
Another similar nice thing to catch would be def f(c: tuple[list[int], list[int], list[int]]):
...
f(tuple([1, 2] for _ in range(3))) recognising that comprehensions into |
Similarly:
|
@Phil-Barber Great example, but maybe that should be a separate issue? Since something like this would ideally work: from typing import TypeVar
T = TypeVar('T')
def f(x: T) -> T: ...
class X: pass
class Y: pass
class Z: pass
x = (X(), Y(), Z())
y = tuple(map(f, x))
reveal_type(y) # Ideally: tuple[X, Y, Z] In other words, it doesn't just need to keep track of a count here. |
This adds coverage for most of the public API, but there are still some areas that aren't covered yet. * script_host.py is ignored entirely * host.py is ignored entirely * the msgpack_rpc submodule has partial coverage * there are some Any annotations sprinkled around on some of the more complex functions Funtionality should remain largely unchanged. Notable exceptions are: * Buffer.mark() and Window.cursor now return a tuple instead of a list. This is because there is currently no type for a fixed-size list (python/mypy#7509)
I believe this is a bug
or a mock-up repro if the source is private. We would appreciate
if you try to simplify your case to a minimal repro.
Minimal example to reproduce:
mypy test.py
mypy should evaluate this as correct code
Do you see the same issue after installing mypy from Git master?
$ mypy --version
mypy 0.720
$ python --version
Python 3.7.3
The text was updated successfully, but these errors were encountered: