17

I recently started using type hints in my code, and have so far found them to be (mostly) very helpful.

However, one thing that I really do not like is the syntax to force the type checker to assume that a variable is of a certain type. Given this example:

import itertools
from typing import Iterable, Tuple
x: Iterable[Tuple[str, str]] = itertools.combinations('abc', 2)
# error: Incompatible types in assignment (expression has type "Iterable[Tuple[str, ...]]", variable has type "List[Tuple[str, str]]")

As far as I can tell, the recommended way to work around this is to explicitly cast the object to force the type checker to use the specified type, e.g.:

import itertools
from typing import Iterable, Tuple, cast
x = cast(Iterable[Tuple[str, str]], itertools.combinations('abc', 2))

I personally find this solution to be a bit gross. My primary concern is that, to the inexperienced reader, it is not clear that the cast is purely there to help the static analyzer. (If I didn't already know, I would assume based on the name and context that it is converting and doing a copy into an object of the specified type, when really there is no runtime cost.)

cast looks like any old function call. When I see that a function is being called on a value, I expect the value to be mutated and/or some other side-effects to occur, but in this case the only side effect is that mypy stops complaining. Type hints themselves have a distinct syntax, but I feel that this blurs the lines with a mixture of the new typing syntax and traditional python syntax. (It's already a bit blurry since you have to import the types and can compose them, but that's another discussion.)

Is there an alternative syntax for cast-like behavior? I haven't found anything, but I was hoping for something like:

x1 = itertools.combinations('abc', 2)) # cast: Iterable[Tuple[str, str]] 

x2: Iterable[Tuple[str, str]] = itertools.combinations('abc', 2)) # type: cast

x3: Cast[Iterable[Tuple[str, str]]] = itertools.combinations('abc', 2))
juanpa.arrivillaga
  • 88,713
  • 10
  • 131
  • 172
0x5453
  • 12,753
  • 1
  • 32
  • 61
  • I do not believe there is any alternative, no. It was probably a compromise solution. You probably should tag this with `mypy` – juanpa.arrivillaga Apr 01 '19 at 19:23
  • Assuming a function will have side effects would be a good habit to break, as it stems from the decades-long practice in the programming language community of abusing the term "function". The name `cast` itself, though, is meant to suggest that it *doesn't* do anything except return the same value. I also doubt *this* is what an inexperienced user will trip over regarding type hinting. – chepner Apr 01 '19 at 19:31
  • Are these casts even needed? `mypy` ships with the `typeshed`, which [includes a definition for `itertools.combinations`](https://github.com/python/typeshed/blob/master/stdlib/3/itertools.pyi#L103). It's slightly wrong (it declares it an `Iterable`, not an `Iterator`), but that matches your declaration anyway. – ShadowRanger Apr 01 '19 at 19:46
  • @chepner: I'm not sure what your point is? The `typeshed` should be declaring all `product`, `permutations` and `combinations` related things as `Iterator`s; it's a mistake that `combinations` and `combinations_with_replacement` are declared as `Iterable`s (`product`/`permutations` are correctly declared as `Iterator`s). – ShadowRanger Apr 01 '19 at 19:57
  • 1
    @ShadowRanger The cast is needed is to set the size of the inner tuple (`Tuple[str, str]` instead of `Tuple[str, ...]`). Normally the variadic size would make sense since `combinations` can depend on a runtime value, but in my case I always only ever want sequences of length 2. – 0x5453 Apr 01 '19 at 20:17
  • I don't know if it's really any less ugly, but you could do `x: Iterable[Tuple[str, str]] = ((a, b) for (a, b) in itertools.combinations('abc', 2))`. – Nathan Vērzemnieks Apr 21 '19 at 21:07

1 Answers1

2

Actually the latest version of Mypy does return the correct type Iterator[Tuple[str, str]].

This change was introduced to Typeshed in PR https://github.com/python/typeshed/pull/4309.

If you cannot update mypy to the latest version you can checkout the latest version from typeshed and use the config option custom_typeshed_dir.

See https://mypy.readthedocs.io/en/stable/config_file.html#confval-custom_typeshed_dir for more details.

Sebastian Kreft
  • 7,819
  • 3
  • 24
  • 41