Skip to content

Unpacking tuples of variable length #1178

Closed

Description

@mihneagiurgea

Given the following code:

from typing import Any, Dict, Tuple, Union

VarTuple = Union[Tuple[int, int], Tuple[int, int, int]]

def make_tuple():
    # type: () -> VarTuple
    return None

x = make_tuple()
a = b = c = 0
if len(x) == 3:
    a, b, c = x
else:
    a, b = x
    c = 0

mypy complains about the following error:

sandbox.py:12: error: 'Union[Tuple[builtins.int, builtins.int], Tuple[builtins.int, builtins.int, builtins.int]]' object is not iterable
sandbox.py:14: error: 'Union[Tuple[builtins.int, builtins.int], Tuple[builtins.int, builtins.int, builtins.int]]' object is not iterable

Activity

  1. JukkaL commented on Jan 28, 2016

    @JukkaL
    Collaborator

    The error message is incorrect. However, mypy may not be able to type check this properly since it doesn't recognize the len(x) == 3 check. So there seem to be two related issues:

    • Union types aren't recognized as iterables when type checking assignment statements.
    • len(x) == n checks can't be used to narrow down tuple types or unions. These could work similarly to isinstance checks.

    The recommended way to work around the issue is to use casts:

    if len(x) == 3:
        a, b, c = cast(Tuple[int, int, int], x)
    else:
        a, b = cast(Tuple[int, int], x)
        c = 0
    
  2. modified the milestones: , on Mar 1, 2016
  3. JukkaL commented on Nov 30, 2016

    @JukkaL
    Collaborator

    Also, mypy could recognize len(x) when x is a variable-length tuple. Example:

    def f(x: Tuple[int, ...]) -> None:
        assert len(x) == 3 
        # Type of x should be Tuple[int, int, int] now
        ...
    
  4. removed this from the milestone on Mar 29, 2017
  5. neiljp commented on Aug 21, 2017

    @neiljp

    I just ran into this issue and was going to post the following test case, but didn't realise that len(x) wasn't yet a way to distinguish among unions:

    def f_4(a: Tuple[int, int, int, int]) -> None:
        pass
    
    def f_2_4(a: Union[Tuple[int, int], Tuple[int, int, int, int]]) -> None:
        if len(a) == 2:
            aa = a + (0,0)
        else:
            aa = a
        f_4(aa)
    
  6. added a commit that references this issue on Aug 17, 2018
  7. pgunn commented on Apr 4, 2019

    @pgunn

    I'm getting a different error, but I'm guessing this is the same issue:

    #!/usr/bin/env python
    
    from typing import Tuple, Union
    
    ####################
    # test
    
    def patient_caller():
    	a,b,c = annoying_returner(calculate_more=True)
    	print(f"I got {a}, {b}, {c}")
    
    def annoying_returner(calculate_more:bool=False) -> Union[Tuple[int,int], Tuple[int,int,int]]:
    	if calculate_more:
    		return 1,2,3
    	else:
    		return 4,5
    
    def main():
    	patient_caller()
    
    ##########
    
    main()
    
    mypytest.py:9: error: Need more than 2 values to unpack (3 expected)
    

    (I have a mypy.ini specifying check_untyped_defs)

  8. 10 remaining items

  9. rodo-hf commented on Apr 15, 2021

    @rodo-hf

    If your list to unpack is never greater than your left side and you know how many variables are on the left side, then you can do this (assuming there are 5 variables on the left side):

    list_to_unpack = [1, 2, 3]
    
    a, b, c, d, e = list_to_unpack + (5 - len(list_to_unpack)) * [None]
    
    print(a)
    print(b)
    print(c)
    print(d)
    print(e)
    
    # output
    # 1
    # 2
    # 3
    # None
    # None

    if you also want to catch the cases where the list is bigger than the variables, then you can do as follows:

    list_to_unpack = [1, 2, 3, 4, 5, 6]
    count_variables = 5
    a, b, c, d, e = list(list_to_unpack + (count_variables - len(list_to_unpack)) * [None])[:count_variables]
  10. added a commit that references this issue on Oct 22, 2021
  11. wxgeo commented on Mar 20, 2023

    @wxgeo

    I ran into a similar issue today.

    Minimal example:

    transition: tuple[int, int] | tuple[int, int, str] = (1, 1, "a")
    if len(transition) == 3: 
        print(transition[2])

    An error is raised by mypy for last line: error: Tuple index out of range [misc]

  12. arvindsree commented on May 2, 2023

    @arvindsree
  13. fofoni commented on May 2, 2023

    @fofoni
  14. arvindsree commented on May 4, 2023

    @arvindsree
  15. fofoni commented on May 5, 2023

    @fofoni
  16. arvindsree commented on May 5, 2023

    @arvindsree
  17. JelleZijlstra commented on May 5, 2023

    @JelleZijlstra
    Member

    This is getting very off-topic. Let's leave this issue focused on narrowing tuple types with len().

  18. w238liu commented on Aug 25, 2023

    @w238liu

    Just wondering if this issue has been resolved? I still get a mypy error for the following code snippet:

    from typing import Tuple
    
    
    def f0(n: int) -> Tuple[int, ...]:
        return tuple(range(n))
    
    
    def f2() -> Tuple[int, int]:
        ret = f0(2)
    
        assert len(ret) == 2
    
        return ret
    
    
    if __name__ == '__main__':
        print(f2())

    Save the code in tmp/mypy_test/main.py and run mypy tmp/mypy_test/main.py, I get the following error message:
    tmp/mypy_test/main.py:13: error: Incompatible return value type (got "Tuple[int, ...]", expected "Tuple[int, int]") [return-value]

  19. JelleZijlstra commented on Aug 25, 2023

    @JelleZijlstra
    Member

    It's not resolved, that's why this issue is still open.

  20. w238liu commented on Aug 25, 2023

    @w238liu
  21. added a commit that references this issue on Oct 21, 2023
    a3af87b
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions