PYTHON PROGRAMMING

The tuple is an immutable collection type in Python. It’s one of the three most popular collection types in Python, along with the list and the dictionary. While I think that many beginning and intermediate developers know much about these two types, they may have problems with truly understanding what tuples are and how they work. Even advanced Python developers do not have to know everything about tuples – and given the type’s specificity, this does not come as a surprise to me.
As a beginning and even intermediate Python developer, I did not know much about the tuple. Let me show you an example; imagine I wrote a fragment of code similar to the following:
from pathlib import Path
ROOT = Path(__file__).resolve().parent
basic_names = [
"file1",
"file2",
"file_miss_x56",
"xyz_settings",
]
files = [
Path(ROOT) / f"{name}.csv"
for name in basic_names
]
As you see, I used a list literal to define the basic_names
list – but why not a tuple literal? It would look like below:
basic_names = (
"file1",
"file2",
"file_miss_x56",
"xyz_settings",
)
The main thing we know about the tuple is that it’s immutable – and the code itself suggests that the basic_names
container will not change. Thus, a tuple seems more natural here than a list, doesn’t it? So, is there any practical difference between the two approaches? Like in performance, safety, or anything else?
Such gaps in knowledge make us worse programmers. This article aims to help you become a better programmer, by helping you learn about one of the most important data type in Python, but one that many don’t know much about: the tuple. My aim is to make this article as thorough as possible from a practical point of view. So, for example, we will not talk about the details of C implementation of the tuple, but we will talk about the details of using tuples in Python.
Tuples are a rich topic. Thus, I will split the knowledge about it into two parts – and two articles. Here are the topics I will cover in the first part – that is, here:
- The basic of tuples.
- Using tuples: tuple unpacking and tuple methods.
So, we will focus on the basics here. In the second part, I cover more advanced topics of tuples, such as inheriting from tuple, tuple performance and tuple comprehensions. You will find it here:
Python Tuple, The Whole Truth and Only Truth: Let’s Dig Deep
The basics of tuples
A tuple is a container of values, similar to a list. In his great book entitled Fluent Python, L. Ramalho explains that tuples were created to be immutable lists, and that this term describes the nature of tuples well. But he also says that tuples are not just immutable lists; they are much more than that.
In particular, tuples can be used as records without field names. This means that we can have a record with several unnamed fields. Certainly, such a tuple-based record makes sense only when it is clear what each field represents.
When you want to create a tuple in Python using a tuple literal, you need to use parentheses ()
instead of square brackets []
, as you would when creating a list¹:
>>> x_tuple_1 = (1, 2, 3)
>>> x_tuple_1
(1, 2, 3)
>>> x_tuple_2 = ([1, 2], 3)
>>> x_tuple_2
([1, 2], 3)
Here, x_tuple_1 = (1, 2, 3)
creates a three-element tuple containing numbers 1
, 2
, and 3
; x_tuple_2 = ([1, 2], 3)
creates a two-element tuple with two values: a list [1, 2]
and number 3
. As you see, you can use objects of any types in a tuple. You can even create a tuple of empty tuples:
>>> tuple((tuple(), tuple()))
((), ())
Although, to be honest, I do not know why you would want to do this.
Okay, so above we used a tuple literal. A second method of creating a tuple is using the built-in tuple()
class. Enough to provide an iterable as an argument, and this will convert the iterable to a tuple:
>>> tuple([1, 2, 5])
(1, 2, 5)
>>> tuple(i for i in range(5))
(0, 1, 2, 3, 4)
To access values in a tuple, you can use typical indexing: x_tuple_1[0]
will return 1
while x_tuple_2[0]
will return a list, [1, 2]
. Note that since x_tuple_2[0]
is a list, you can access its elements using its indices – so, you will use multiple (here, double) indexing; for example, x_tuple_2[0][0]
will return 1
while x_tuple_2[0][1]
will return 2
.
The biggest difference between lists and tuples is that lists are mutable, so you can change them, while tuples are immutable, so you cannot change them:
>>> x_list = [1, 2, 3]
>>> x_tuple = (1, 2, 3)
>>> x_list[0] = 10
>>> x_list
[10, 2, 3]
>>> x_tuple[0] = 10
Traceback (most recent call last):
...
TypeError: 'tuple' object does not support item assignment
As you see, you cannot use item assignment with tuples. This feature makes tuples less error prone than lists, as you can be sure (actually, almost sure, as we will discuss below) that tuples will not change. You can be sure, however, that their length will not change.
There is a common interview question about tuples: Since tuples are immutable, you cannot change their values, right? And the answer to this very question is: Well…
This is because you can change values of mutable elements of a tuple:
>>> x_tuple = ([1, 2], 3)
>>> x_tuple[0][0] = 10
>>> x_tuple
([10, 2], 3)
>>> x_tuple[1] = 10
Traceback (most recent call last):
...
TypeError: 'tuple' object does not support item assignment
So, although tuples are immutable, if their elements are not, you can change these elements, and so, at least indirectly, you can change the tuple. This makes it possible to change an unchangeable…
If you’re feeling confused, at least clothe yourself with the fact that you’re not alone. You’re just one of many. However, this kind of immutability makes sense, at least theoretically, so let me explain what’s going on here.
The whole truth lies in the following. Like other collections, tuples do not contain objects but references to them; being immutable means being immutable in terms of these references. Therefore, once created, a tuple will always contain the same set of references.
- In theory, when an object being referenced to by one of a tuple’s references changes, the tuple remains the same: it’s still the very same tuple, with the very same references.
- In practice (that is, from our typical/natural point of view), when an object being referenced to by one of a tuple’s references changes, the tuple seems to have changed: despite the very same references, one of the objects changed, so, in practice, the tuple looks different than it did before this change. But theoretically, the tuple (a collection of references) has not changed whatsoever.
Like other collections, tuples do not contain objects but references to them; being immutable means being immutable in terms of these references.
Okay, now that we know how tuple immutability works, we should remember to think that way about tuples, too. But knowing something does not have to mean that getting used to it will be easy. It’s not that easy to think of immutability that way. Remember, from now on you should remember that a tuple is an immutable collection of references to objects, not an immutable collection of objects. The values of objects a tuple contains can actually change – but the objects must stay the same… Already feeling a headache? It’s just the beginning…
Let’s think of a typical length of tuples. To add some context, however, we should consider what it looks like in lists. I think it’s safe to say that both short and long lists are frequently used. You can create a list using various methods, like a literal, a for
loop, the list()
method, and a list comprehension.
Immutable, tuples do not work like that. You cannot update them in a for
loop (unless you’re updating their mutable elements) or a comprehension. You can create a tuple in two ways, using a tuple literal, like here:
>>> x = (1, 56, "string")
or calling the tuple()
class (tuple()
is a callable class) to an iterable:
>>> x = tuple(x**.5 for x in range(100))
My guess is that the former use case is far more frequent. Perhaps the most frequent use of the tuple is to return values from a function, especially when it’s two or three values (you would seldom (if ever) do this for ten values).
When a tuple literal is short, quite often the parentheses are omitted:
>>> x = 1, 56, "string"
This approach is often used with return
statements, but not only. Is any of the two – with or without parentheses – better? Generally, no; but it depends on the situation. Sometimes the parentheses will make the code clearer, and some other times their absence will.
Do remember about non-parentheses tuples, as they can be a source of bugs that are difficult to find; see here:
To put it simply, when you forget about a comma at the end of a line, you may be using a tuple with an object instead of the object itself:
>>> x = {10, 20, 50},
You may think that x
is a set with three elements, but in fact it’s a tuple with one element:
>>> x
({10, 20, 50},)
As you see, this one single comma put after instead of before the right curly bracket made x
a one-element tuple.
Tuples in action
Tuples offer fewer methods than lists, but still quite a few. Some of them are better known than others; some are even very little known and used rather infrequently. In this section, we we discuss two important aspects of using tuples: tuple methods and unpacking tuples.
Unpacking
A fantastic feature of tuples is tuple unpacking. You can use it to assign a tuple’s values to multiple names at once. For example:
>>> my_tuple = (1, 2, 3,)
>>> a, b, c = my_tuple
Here, a
would become 1
, b
would become 2
, and c
would become 3.
Consider the below example:
>>> x_tuple = ([1, 2], 3)
>>> x, y = x_tuple
>>> x
[1, 2]
>>> y
3
You can also use special unpacking syntax using the asterisk, *
:
>>> x_tuple = (1, 2, 3, 4, 5)
>>> a, b* = x_tuple
>>> a
1
>>> b
[2, 3, 4, 5]
>>> *a, b = x_tuple
>>> a
[1, 2, 3, 4]
>>> b
5
>>> a, *b, c = x_tuple
>>> a
1
>>> b
[2, 3, 4]
>>> c
5
As you see, when you attach the asterisk *
to a name, it’s like saying, "Unpack this very item and all next ones to this name." So:
a, b*
means unpack the first element toa
and all the remaining ones tob
.*a, b
means unpack the last element tob
and all those before toa
.a, *b, c
means unpack the first element toa
, the last element toc
, and all the in-between elements tob
.
With more elements in a tuple, you can consider more scenarios. Imagine you have a tuple of seven elements, and you’re interested in the first two and the last one. You can use unpacking to get and assign them to names in the following way:
>>> t = 1, 2, "a", "ty", 5, 5.1, 60
>>> a, b, *_, c = t
>>> a, b, c
(1, 2, 60)
Note here one more thing. I used *_
, as I needed to extract only these three values, and the other ones can be ignored. Here, the underscore character, _
, means exactly that: I don’t care what these other values from the tuple are, and so let’s ignore them. If you use a name instead, the reader of the code would think that the name is used somewhere in the code – but also your IDE would scream at you for assigning values to a name that isn’t used anywhere in the scope².
Tuple unpacking can be used in various scenarios, but it’s particularly useful when you’re assigning values returned from a function or method that returns a tuple. The below example shows the usefulness of unpacking values returned from a function/method.
First, let’s create a Rectangle
class:
>>> @dataclass
... class Rectangle:
... x: float
... y: float
... def area(self):
... return self.x * self.y
... def perimeter(self):
... return 2*self.x + 2*self.y
... def summarize(self):
... return self.area(), self.perimeter()
>>> rect = Rectangle(20, 10)
>>> rect
Rectangle(x=20, y=10)
>>> rect.summarize()
(200, 60)
As you see, the Rectangle.summarize()
method returns two values organized in a tuple: the rectangle’s area and perimeter. If we want to assign these values to names, we could do as follows:
>>> results = rect.summarize()
>>> area = result[0] # poor!
>>> perimeter = result[1] # poor!
However, the above approach is not a good one, among others for clarity reasons, and we can do it much more effectively using tuple unpacking:
>>> area, perimeter = rect.summarize()
>>> area
200
>>> perimeter
60
As you can see, it’s clearer and shorter: just one line instead of three. In addition, it does not use indexing to get the values from the tuple. Indexing decreases readability, and it’d be better to use names instead of positions. We will discuss this below, in the section on inheriting from the tuple
class and on named tuples. But remember that when a function/method returns a tuple – quite a frequent situation – you should unpack these values instead of assign them directly using tuple indexing.
One more example, also using a dataclass
³:
>>> from dataclasses import dataclass
>>> KmSquare = float
>>> @dataclass
... class City:
... lat: float
... long: float
... population: int
... area: KmSquare
... def get_coordinates(self):
... return self.lat, self.long
>>> Warsaw = City(52.2297, 21.0122, 1_765_000, 517.2)
>>> lat, long = Warsaw.get_coordinates()
>>> lat
52.2297
>>> long
21.0122
The above examples show the most frequent use cases of tuple unpacking. Nonetheless, we can sometimes need to unpack values from a nested data structure based on tuples. Consider the following example. Imagine that we have a list of cities like above, a city being represented by a list inside a dictionary, not a dataclass
:
>>> cities = {
... "Warsaw": [(52.2297, 21.0122), 1_765_000, 517.2],
... "Prague": [(50.0755, 14.4378), 1_309_000, 496],
... "Bratislava": [(48.1486, 17.1077), 424_428_000, 367.6],
... }
As you see, we have the coordinates of the cities organized as tuples inside the list. We can use nested unpacking to get the coordinates:
>>> (lat, long), *rest = cities["Warsaw"]
>>> lat
52.2297
>>> long
21.0122
Or we may need also the area:
>>> (lat, long), _, area = cities["Warsaw"]
>>> lat, long, area
(52.2297, 21.0122, 517.2)
Again, I have again used the underscore character, _
, to assign a value we don’t need.
Note that what we do with *args
is exactly unpacking. By putting *args
inside a function’s arguments, you let the users know they can use any arguments there:
>>> def foo(*args):
... return args
>>> foo(50, 100)
(50, 100)
>>> foo(50, "Zulu Gula", 100)
(50, 'Zulu Gula', 100)
Here, *args
collects all the positional (not keyword!) arguments into the args
tuple. This return
statement enables us to see these arguments in the args
tuple.
One more thing: unpacking is not reserved for tuples, and you can use it to other iterables, too:
>>> a, *_, b = [i**2 for i in range(100)]
>>> a, b
(0, 9801)
>>> x = (i for i in range(10))
>>> a, b, *c = x
>>> c
[2, 3, 4, 5, 6, 7, 8, 9]
Tuple methods
Python beginners learn about tuples rather quickly. With time, they learn a little more about them, mainly their immutability and its consequences. But many developers do not know all the methods the tuple
class offers. To be honest, before writing this article, I did not know them when I was considering myself quite an advanced developer. But it’s good to know these methods – and this subsection aims to help you learn them.
It does not mean you need to use all of them. But it’s good, for example, to remember that you can use in-place operations on tuples, and what they lead to. This knowledge is enough to recall that there are only two in-place operations for tuples: in-place concatenation and in-place repeated concatenation.
To learn the methods, let’s peek into Fluent Python again. We will find there a nice table with the comparison of the list’s and the tuple’s methods, from which we can extract the latter. Hence, below, you will find a complete list of methods of the tuple
class, each accompanied by one or more simple examples.
Get length: len(x)
>>> len(y)
7
Concatenation: x + y
>>> x = (1, 2, 3)
>>> y = ("a", "b", "c")
>>> z = x + y
>>> z
(1, 2, 3, 'a', 'b', 'c')
Repeated concatenation: x * n
>>> x = (1, 2, 3)
>>> x * 3
(1, 2, 3, 1, 2, 3, 1, 2, 3)
Reversed repeated concatenation: n * x
>>> x = (1, 2, 3)
>>> 3 * x
(1, 2, 3, 1, 2, 3, 1, 2, 3)
In-place concatenation: x += y
>>> x = (1, 2, 3)
>>> y = ("a", "b", "c")
>>> x += y
>>> x
(1, 2, 3, 'a', 'b', 'c')
The syntax of in-place concatenation may suggest that we’re dealing with the same object: we started with tuple x
that was equal to (1, 2, 3)
; after concatenating y
, x
was still a tuple, but it contained six values: (1, 2, 3, "a", "b", "c")
. Since we discussed tuple immutability, we know that the x
before and the x
after were two different objects.
We can easily check this using the following simple test. It uses the two objects’ id
s: if they have the same id
, they are one and the same object, but if the id
s differ, x
before the in-place concatenation and x
after it are two different objects. Let’s do it:
>>> x = (1, 2, 3)
>>> first_id = id(x)
>>> y = ("a", "b", "c")
>>> x += y
>>> second_id = id(x)
>>> first_id == second_id
False
The two id
s differ, meaning that x
after the in-place operation is a different object than x
before it.
In-place repeated concatenation: x *= n
>>> x = (1, 2, 3)
>>> x *= 3
>>> x
(1, 2, 3, 1, 2, 3, 1, 2, 3)
What I wrote above applies here, too: although we see just one name here, x
, we have two objects: the before x
and the after x
.
Contains: __ in
>>> x = (1, 2, 3)
>>> 1 in x
True
>>> 100 in x
False
Count occurrences of an element: x.count(element)
>>> y = ("a", "b", "c", "a", "a", "b", "C")
>>> y.count("a")
3
>>> y.count("b")
2
Get item at position: x[i]
(`x.getitem__(i)`)
>>> y[0]
'a'
>>> y[4], y[5]
('a', 'b')
Find position of first occurrence of element
: x.index(element)
>>> y = ("a", "b", "c", "a", "a", "b", "C")
>>> y.index("a")
0
>>> y.index("b")
1
Get iterator: iter(x)
(x.__iter__()
)
>>> y_iter = iter(y)
>>> y_iter # doctest: +ELLIPSIS
<tuple_iterator object at 0x7...>
>>> next(y_iter)
'a'
>>> next(y_iter)
'b'
>>> for y_i in iter(y):
... print(y_i, end=" | ")
a | b | c | a | a | b | C |
Support for optimized serialization with pickle
: x.__getnewargs__()
This method is not to be used like the above ones, in a direct way. Instead, it’s used during pickling to optimize tuples’ pickling, like in the below toy example:
>>> import pickle
>>> with open("x.pkl", "wb") as f:
... pickle.dump(x, f)
>>> with open("x.pkl", "rb") as f:
... x_unpickled = pickle.load(f)
>>> x_unpickled
(1, 2, 3)
In his fantastic book Fluent Python (2nd edition), Luciano Ramalho lists 15 methods that that the list has but the tuple does not— but this one, the optimized pickling optimization, is the only method that the tuple has and the list does not.

Conclusion
In this article, we’ve discussed the basics of one of the most common Python collection types, the tuple. I hope you’ve enjoyed this – and if you did, be aware that what we’ve discussed was not only basic but also, how to say it, uncontroversial.
There’s much more into tuples, however, and some of it is not as clear as what we’ve learned from this article. We will discuss this in the continuation of this article. You will see there that tuples are not an easy topic, as you might think after reading this article. No, in my opinion tuples are more controversial than any other built-in type. Perhaps even tuples are overused – but I will let you decide yourself after reading the next article. But to be honest, there are things about tuples I don’t like. In fact, I will be a little harsh for tuples… Rather much… Maybe even too much?
I hope I have intrigued you enough for you to read the continuation of this article. You will find it here:
Python Tuple, The Whole Truth and Only Truth: Let’s Dig Deep
Thank you for reading. If you enjoyed this article, you may also enjoy other articles I wrote; you will see them here. If you want to join Medium, please use my referral link below:
Footnotes
¹ Note that in many code blocks, like the one above, I use doctest
testing, in order to assure that the examples work correctly. You can read more about doctest
in the module’s documentation and this introductory article published in Towards Data Science.
² Note that I wrote "in scope" and not "in code". This is because while we do not need these values here, we can need them elsewhere in the code, in some other scope (e.g., in another function). Using particular unpacking in a particular scope only affects this scope; hence, we can unpack the same iterable once more, in another scope, and this unpacking can be different.
³ In the code block, you will find the KmSquare
type alias. I used it to increase the readability of the floats used to define a city. You can read more about such type hinting and type aliasing here.
Resources
Python Documentation Testing with doctest: The Easy Way
A Guide to Python Comprehensions