130

I have a function that takes a tuple of different lengths as an argument:

from typing import Tuple


def process_tuple(t: Tuple[str]):
    # Do nasty tuple stuff

process_tuple(("a",))
process_tuple(("a", "b"))
process_tuple(("a", "b", "c"))

When I annotate function like mentioned above, I get these error messages

fool.py:9: error: Argument 1 to "process_tuple" has incompatible type "Tuple[str, str]"; expected "Tuple[str]"
fool.py:10: error: Argument 1 to "process_tuple" has incompatible type "Tuple[str, str, str]"; expected "Tuple[str]"

process_tuple really works with tuples and I use them as immutable lists of variable length. I haven't found any consensus on this topic on the internet, so I wonder how should I annotate this kind of input.

Alex Waygood
  • 6,304
  • 3
  • 24
  • 46
Montreal
  • 2,143
  • 5
  • 18
  • 28

3 Answers3

232

We can annotate variable-length homogeneous tuples using the ... literal (aka Ellipsis) like this:

def process_tuple(t: Tuple[str, ...]):
    ...

or for Python3.9+

def process_tuple(t: tuple[str, ...]):
    ...

After that, the errors should go away.

From the docs:

To specify a variable-length tuple of homogeneous type, use literal ellipsis, e.g. Tuple[int, ...]. A plain Tuple is equivalent to Tuple[Any, ...], and in turn to tuple.

Azat Ibrakov
  • 9,998
  • 9
  • 38
  • 50
  • 17
    This actually *is* a little bit counter-intuitive and counter-logical. If we assume that `List[str]` is okay for variable length lists, then why `Tuple[str]` is not okay for variable length tuple? And `(type(("a", "a")) == type(("a", "a", "a"))` yields `True`. – Montreal Feb 19 '19 at 06:25
  • 27
    @Montreal: that's because `tuple`s and `list`s serve for different purposes: `tuple`s are heterogeneous containers (like positional arguments to an arbitrary function or a single table record from RDBMS, or in math world -- elements of cartesian product of different sets (or a union of cartesian products), so each coordinate may have a different type, but their number is usually fixed), while `list`s are homogeneous (like collection of same table records or a finite sequence of elements of some set) – Azat Ibrakov Feb 19 '19 at 07:31
  • 6
    @Montreal: so `Tuple[str]` is a single-`str`-object `tuple`, while `List[str]` is a collection of arbitrary number of `str` objects – Azat Ibrakov Feb 19 '19 at 07:32
  • 2
    Yes tuples and lists are usually used for different purposes but I think many Python programmers will learn about this convention only after a long time if at all. The reason is that the difference is not given by the language properties but rather by a habit which IMHO is not mentioned prominently enough in the documentation. --- I know only one place in the documentation: *Tuples ... usually contain a heterogeneous sequence of elements. ... Lists ... their elements are usually homogeneous.* https://docs.python.org/3/tutorial/datastructures.html#tuples-and-sequences – pabouk - Ukraine stay strong Nov 19 '22 at 13:54
  • I understand your point, @pabouk-Ukrainestaystrong, but despite the Zen of Python claiming "_There should be one-- and preferably only one --obvious way to do it_", this is rarely **enforced** to be the case, and so much of _Pythonic_ Python is just by learning the way things **should** be. I would also say that this `tuple` vs `list` usage goes far beyond Python. So it is a good best practice to learn in coding more generally. – Mike Williamson May 09 '23 at 11:49
12

Python 3.9+

Use tuple:

def process_tuple(t: tuple[str, ...]):
    pass

Since Python 3.9, typing.Tuple is deprecated. The documentation for typing.Tuple states:

Deprecated since version 3.9: builtins.tuple now supports [].

Python 3.8 and earlier

If you are on Python 3.8 or earlier, you should still use typing.Tuple:

from typing import Tuple

def process_tuple(t: Tuple[str, ...]):
    pass
gertvdijk
  • 24,056
  • 6
  • 41
  • 67
BrokenBenchmark
  • 18,126
  • 7
  • 21
  • 33
8

In addition to the Ellipsis answer as posted by Azat you could make it more explicit by using @typing.overload or typing.Union

from typing import Tuple


@overload
def process_tuple(t: Tuple[str]):
    # Do nasty tuple stuff

@overload
def process_tuple(t: Tuple[str, str]):
    ...

Or with the Union:

from typing import Tuple, Union


def process_tuple(t: Union[Tuple[str], Tuple[str, str], Tuple[str, str, str]]):
    # Do nasty tuple stuff
Wolph
  • 78,177
  • 11
  • 137
  • 148