4

The following code:

from typing import Union


def process(actions: Union[list[str], list[int]]) -> None:
    for pos, action in enumerate(actions):
        act(action)


def act(action: Union[str, int]) -> None:
    print(action)

generates a mypy error: Argument 1 to "act" has incompatible type "object"; expected "Union[str, int]"

However when removing the enumerate function the typing is fine:

from typing import Union


def process(actions: Union[list[str], list[int]]) -> None:
    for action in actions:
        act(action)


def act(action: Union[str, int]) -> None:
    print(action)

Does anyone know what the enumerate function is doing to effect the types? This is python 3.9 and mypy 0.921

James Roberts
  • 195
  • 1
  • 4
  • 13
  • Statically, you can't write a more specific return type for `enumerate.__next__` than `Tuple[int, Any]`, because the runtime return type is specified solely by the runtime argument to `enumerate`. Using a generic wouldn't help, because the iterable being enumerated doesn't need to have a homogenous type. I think `mypy` would need to be modified to take a static type hint for the argument into account in order to infer that `action` has type `Union[str, int]` rather than `Any`. – chepner Jan 07 '22 at 16:01
  • FWIW, if you change `Union[list[str], list[int]]` to `list[Union[str, int]]` or (arguably better yet) `Iterable[Union[str, int]]`, then mypy can work out that the type of `action` is `Union[str, int]`. – jjramsey Jan 07 '22 at 16:04
  • @jjramsey That changes the type, though. It would make `[1, 'x', 3]` valid where before it was not. (But it does give me an idea...) – chepner Jan 07 '22 at 16:08

3 Answers3

3

enumerate.__next__ needs more context than is available to have a return type more specific than Tuple[int, Any], so I believe mypy itself would need to be modified to make the inference that enumerate(actions) produces Tuple[int,Union[str,int]] values.

Until that happens, you can explicitly cast the value of action before passing it to act.

from typing import Union, cast

StrOrInt = Union[str, int]

def process(actions: Union[list[str], list[int]]) -> None:
    for pos, action in enumerate(actions):
        act(cast(StrOrInt, action))


def act(action: Union[str, int]) -> None:
    print(action)

You can also make process generic (which now that I've thought of it, is probably a better idea, as it avoids the overhead of calling cast at runtime).

from typing import Union, cast, Iterable, TypeVar

T = TypeVar("T", str, int)

def process(actions: Iterable[T]) -> None:
    for pos, action in enumerate(actions):
        act(action)


def act(action: T) -> None:
    print(action)

Here, T is not a union of types, but a single concrete type whose identity is fixed by the call to process. Iterable[T] is either Iterable[str] or Iterable[int], depending on which type you pass to process. That fixes T for the rest of the call to process, which every call to act must take the same type of argument.

An Iterable[str] or an Iterable[int] is a valid argument, binding T to int or str in the process. Now enumerate.__next__ apparently can have a specific return type Tuple[int, T].

chepner
  • 497,756
  • 71
  • 530
  • 681
0

I don't know how it's affecting the types. I do know that using len() can work the same way. It is slower but if it solves the problem it might be worth it. Sorry that it's not much help

Moose
  • 5
  • 4
0

Seems like mypy isn't able to infer the type and generalizes to object. Might be worth opening an issue at their side. As a workaround you could annotate 'action'. This would remove the error. Does it work if you import the (legacy) List from typing?

Simon
  • 5,464
  • 6
  • 49
  • 85