# Tuples

Now that we know about lists, let's look at their immutable sibling, tuples.

The name tuple originates from the suffix of the sequence of n elements (single, couple/double/pair, triple, quadruple, quintuple, sextuple, septuple, ... ntuple)

Tuples (literals) are written just like lists, but without the square brackets:

```>>> 1, 2, 3
(1, 2, 3)
>>> 1, # single element tuple with a trailing comma - not recommended
(1,)
```

The tuple literals are often written with round brackets. It is also the way Python prints them:

```>>> (1, 2, 3)
(1, 2, 3)
>>> (1,) # single element with a trailing comma with brackets - recommended
(1,)
>>> () # empty tuple - cannot be written as a literal without brackets
()
```

Alternatively, you can also create tuples with the `tuples` constructor:

```>>> tuple()  # empty tuple
()
>>> tuple(range(3)) # accepts anything which can be used in for loops
(0, 1, 2)
```

## Tuples with round brackets or no brackets?

With exception of the empty tuple, tuples are made by the commas rather than by the round brackets. However, we very often need to surround them with round brackets to achieve our goal. The tuples therefore may sometimes look like "lists written with round brackets" even though there are not.

Fell free to use tuples without the brackets but, be aware that there are cases where they can lead to ambiguities and errors:

```>>> 1, 2, 3 + 4, 5, 6
(1, 2, 7, 5, 6)
>>> (1, 2, 3) + (4, 5, 6)
>>> (1, 2, 3, 4, 5, 6)
```

... or:

```>>> 1, 2, 3 == 4, 5, 6
(1, 2, False, 5, 6)
>>> (1, 2, 3) == (4, 5, 6)
False
```

Always use round brackets when passing tuples as function arguments to distinguish them from function arguments:

```>>> sorted(2, 4, 5, 1, 0)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: sorted expected 1 argument, got 5
>>> sorted((2, 4, 5, 1, 0))
[0, 1, 2, 4, 5]
```

It is highly recommended to write single element tuples with brackets '(1,)'. The trailing comma looks like a typo `1,` and the extra round brackets improve clarity of the code.

## How to use tuples

Tuples behave almost like lists, but they cannot be modified. I.e., they don't have methods like `append` and `pop`, elements cannot be assigned or removed:

```>>> my_tuple = (1, 2, 3)

>>> my_tuple.append(1)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'tuple' object has no attribute 'append'

>>> my_tuple.pop(1)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'tuple' object has no attribute 'pop'

>>> my_tuple = -2
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment

>>> del my_tuple
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'tuple' object doesn't support item deletion
```

A "modification" of a tuple always requires creation of a new tuple object.

Tuple manipulations using Python asterisk unpacking:

```>>> sample = (1, 2, 3)

>>> *sample, 4
(1, 2, 3, 4)

>>> *sample, *sample[-2:]
(1, 2, 3, 2, 3)

>>> *sample[:1], -2, *sample[2:]
(1, -2, 3)

>>> *sample[:1], *sample[2:]
(1, 3)
```

`*` in the expressions unpacks the given sequences (list, tuple, range, ... etc.). Mixing list and tuples in these expressions is possible.

FYI, this also works with lists. Try `[*sample[:1], -2, *sample[2:]]`.

... or `+` concatenation of tuples:

```>>> sample = (1, 2, 3)

>>> sample + (4,)
(1, 2, 3, 4)

>>> sample + sample[-2:]
(1, 2, 3, 2, 3)

>>> sample[:1] + (-2,) + sample[2:]
(1, -2, 3)

>>> sample[:1] + sample[2:]
(1, 3)
```

Tuple concatenation with the `+` operator works only with tuples. Mixing list and tuples in these expressions leads to an error.

Tuples can be to converted from/to lists:

```>>> tuple([1, 2, 3]) # list to tuple
(1, 2, 3)

>>> list((1, 2, 3)) # tuple to list
[1, 2, 3]
```

Tuples can be nested. Mixing nested list and tuples is possible:

```>>> 42, ("Hotel", "Golf", "Tango", "Golf")
(42, ('Hotel', 'Golf', 'Tango', 'Golf'))

>>> "John", "Doe", [3, 4 , 2], True
('John', 'Doe', [3, 4, 2], True)

>>> [(1, 1), (0, 4), (4, 5)]
[(1, 1), (0, 4), (4, 5)]
```

Beware that nested mutable value (list) makes a tuple object mutable with all the possibly undesired consequences.

Tuples can be used in `for` loops and they can read individual elements.

```people = 'mom', 'aunt', 'grandmother'
for person in people:
print(person)
print('First is {}'.format(people))
```

Does this look familiar? We have already used tuples in `for greeting in 'Ahoj', 'Hello', 'Hola', 'Hei', 'SYN'`

Short tuples can also be used to simplify conditions:

```if code in (1, 4, 7):
# do something ...
```

is equivalent to

```if code == 1 or code == 4 or code == 7:
# do something ...
```

## Tuples and functions

Tuples are used in functions with variable number of arguments:

```>>> def print_sum(*values):    # values variable holds a tuple of free arguments
...     print(f"sum({values}) = {sum(values)}")
...
>>> print_sum(1, 2)
sum((1, 2)) = 3
>>> print_sum(1, 2, 4, 5)
sum((1, 2, 4, 5)) = 12
```

Tuples are used if we need to return more than one value from a function. You simply declare the return values separated with comma. It looks like you're returning multiple values, but in fact, it is just one tuple being returned:

```def floor_and_remainder(a, b):
return a//b, a%b
```

Such a `floor_and_remainder` function already exists in Python: it's called `divmod` and it's always available (you don't have to import it).

## Multiple assignment

Python can do another trick: if you want to assign values into several variables at once, you can just separate the variables (the left side) by a comma, and the right side can be some "compound" value - for example a tuple:

```floor_number, remainder = floor_and_remainder(12, 5)
```

A tuple is the best for this purpose, but it works with all the values ​​that can be used with a `for` loop:

```x, o = 'xo'                       # is like: x = 'a'; o = 'o'
one, two, three = [1, 2, 3]       # is like: one = 1;  two = 2; three = 3
a, b = 1, 2                       # is like: a = 1;  b = 2
first, *body, last = range(4)     # is like: first = 0 ; body = [1, 2], last = 3
```

`*` in the last assignment grabs anything between the first and last elements and stores it as an array.

## Functions producing tuples

`zip` is an interesting function. It is used in `for` loops, just like the `range` function that returns numbers.

E.g., when `zip` gets two lists (or other values that can be used in a `for` loop), it returns pairs -- the first element of the first list is paired with the first element of the second list, then the second element with the second, the third element with the third and so on.

It is useful when you have two lists with the same structure - the relevant elements "belong" together and you want to process them together:

```>>> list(zip([3, 1, 2], ["a", "b", "c"])
[(3, 'a'), (1, 'b'), (2, 'c')]
```
```people = 'mom', 'aunt', 'grandmother', 'assassin'
properties = 'good', 'nice', 'kind', 'insidious'
for person, property in zip(people, properties):
print ('{} is {}'.format(person, property))
```

Note the tuple multiple assignment in the for statement.

When `zip` gets three lists it will return triplets, and so on.

The other function that returns pairs is `enumerate`. As an argument, it takes a list (or other values that can be used in a `for` loop) and it pairs up the element's index (its order in the list) with the respective element. So the first element will be (0, first element of the given list), then (1, second element), (2, third element) and so on.

```>>> list(enumerate(["a", "b", "c"])) # count from 0
[(0, 'a'), (1, 'b'), (2, 'c')]

>>> list(enumerate(["a", "b", "c"], 1)) # count from 1
[(1, 'a'), (2, 'b'), (3, 'c')]
```
```prime_numbers = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]

for i, prime_number in enumerate(prime_numbers):
print('Prime number on position {} is {}'.format(i, prime_number))
```

## When to use the list and when the tuple?

Lists are used when you do not know in advance how many values you will have, or when there are a lot of values. Simply use lists for list of things, e.g., a list of words in a sentence, a list of contest participants, a list of moves in a game, or a list of cards in a deck.

Tuples are often used for values of different types where each "position" inside the tuple has a different meaning. For example, you can use a list for the letters of the alphabet, but for pairs of index-value from `enumerate`, you'd use a tuple.

The empty and one-element tuples are little strange, but they exist, and Python would not be complete without them.

Both lists and tuples also have limitations or benefits (depending on your point of view). Tuples cannot be changed, and when we will learn how to work with dictionaries, we will find that lists cannot be used as dictionary keys.

Often, it is not entirely obvious which type to use -- in that case, it probably doesn't really matter. Follow your instinct. :)

## Exercise

Let's get back to our `table_parser`.

```SOURCE_TRANSLITERATION_TABLE_UA_GB = """
а a
б b
в v
г h
ґ g
д d
е e
є ye
ж zh
з z
и ȳ
і i
ї yi
й ĭ
к k
л l
м m
н n
о o
п p
р r
с s
т t
у u
ф f
х kh
ц ts
ч ch
ш sh
щ shch
ь ʼ
ю yu
я ya
’ ˮ
"""

def parse_table(source):
""" Parse string table. """
table = []
for line in source.splitlines():
row = line.split()
if row: # ignore empty lines
table.append(row)
return table

table = parse_table(SOURCE_TRANSLITERATION_TABLE_UA_GB)

print(table)
```

1. Modify the `parse_table` function so that it produces table as a list of tuples rather than a list of list.
1. Write a new `add_capitals` function which extend the table by adding capital letters. Use to `string.upper` for the source Cyrillic letters and `str.capitalize` for the Latin transcription.