pythonpython-typingmypyenumerate

enumerate causes incompatible type mypy error


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


Solution

  • 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].