Tuples and Mutability

Quiz: counts toward finishing the lesson. Check: just for practice.

Tuples

Tuples are almost the same as lists, with one big difference: they can't be changed. We'll cover that in the Mutability vs. Immutability section. First, two small differences in syntax. One is that we use square brackets to create a new list, but parentheses to create a new tuple.

The other difference is for tuples that contain only one value. For Python to know it's a tuple, you have to include a trailing comma.

Although parentheses are used for creating a new tuple, we still use square brackets when indexing or slicing a tuple.

Now let's look at type hinting for tuples.

Type hinting with tuples

To type hint tuples in Python, you can use the tuple type and specify the types of the individual elements in the tuple.

In this code, each of the tuples is declared with a type hint using the tuple type, indicating the types of the individual elements in the tuple. The type hint syntax uses a comma-separated list of types within square brackets to specify the types of the elements. For example, tuple[int, int, int] specifies a tuple with three int elements. The tuple[()] syntax is used to specify an empty tuple.

Naturally, the syntax can be a little tedious if you have a large number of items to declare in your tuple.

You can use ... to say that a tuple can contain any number of elements of one type.

Here's an example of using the tuple type to type hint a tuple without declaring individual elements:

In this code, the tuple type is used with the ... syntax to say that each tuple can contain any number of elements of the given type. For example, tuple[int, ...] specifies a tuple that can contain any number of int elements.

Using the tuple type with the ... syntax can make your code more concise and easier to read, and lets you change the number of elements later without editing the hint.

Now let's look at the big difference between lists and tuples in detail.

Mutability vs. Immutability

The main difference between lists and tuples is that lists are mutable, but tuples are immutable. Put more simply, you can change a list, but you can't change a tuple. So far we've seen ways to create new lists based on old lists, but we haven't looked at ways we can change an existing list, with one exception. The sort method doesn't create a new sorted version of the list; it rearranges the existing list into sorted order.

We can't call sort() on tuples because they're immutable.

Mutating a List

How else can we mutate (modify) an existing list? Here are some lists we can demonstrate on.

We can assign a new value to an existing element of a list like this:

We can also put a slice on the left side of the assignment. The value on the right must be an iterable (something you can loop over, such as a list).

The number of elements being assigned can be different than the size of the slice being replaced.

Notice that the slice birds[1:1] is empty, since a slice goes up to the second index, but doesn't include it.

The slice birds[:] would create a slice of the entire list, that is, it would create a copy of the list, which can also be done using list():

We can also append items to the end of a list using append, and delete items from a list using del (notice that append uses method notation, but del uses operator notation).

What about tuples?

None of these assignments will work with tuples because they cannot be mutated. But if tuples are essentially lists that we can't change, why use them at all? One reason is that there are times when an immutable type is required. For example, only immutable types can be used as keys in dictionaries, another way of storing and manipulating data that we'll talk about soon. Another reason is that if the elements shouldn't get changed, using a tuple instead of a list makes sure that won't happen by accident.

Immutable Collections of Mutable Objects

If you have a tuple that contains mutable objects, the tuple itself is immutable because you cannot change which objects it contains, but the mutable objects inside it can still be mutated.

Mutability and Immutability of Function Arguments

In the Function Scope and Parameters page we saw that changing a parameter in a function did not change the value of the variable that was passed in the function call, and said we'd discuss the reason in a later module. The reason is that the variable we passed referred to an immutable type. Integers, like tuples, are immutable and cannot be changed. In fact, lists are the only mutable type we've looked at so far; all the others we've seen are immutable (objects of user-defined classes, which we looked at in module 5, are also mutable). If a variable refers to a list, and we pass that list to a function, then the function can change the actual list that was passed.

Here we see that the variable num_list got changed by the function we passed it to. The function could have changed the list in other ways such as adding or removing elements, but I wanted to show an example very similar to the one I showed in the page on functions, only using a list instead of an integer.

You might have an objection at this point. How can ints, floats, bools and strings be immutable? It seems like we've changed values of those types before. We'll take a look at this in the next section, which talks about object references and identity.

Mutable Default Arguments

In Python, default arguments are evaluated only once, when the function is defined, and not each time the function is called. This can lead to unexpected behavior if the default argument is mutable, such as a list or dictionary.

For example, consider the following code:

In this code, the add_to_new_list function has a default argument lst that is a mutable list. The first call creates the default list lst and appends a value to it. The second call appends to the same list lst, rather than creating a new, separate list.

To avoid this behavior, it is best to use an immutable type, such as None, as the default value for the argument. If a mutable type is desired, the default value should be set to None, and a new mutable object should be created inside the function if the default value is encountered.

In this code, the add_to_new_list function has a default argument lst that is set to None. If the default value is encountered, a new list is created inside the function. This ensures that each call creates a separate, independent list, avoiding the unexpected behavior.

Check your understanding. Predict the result. Remember when a default argument is created.

def add_item(item: str, items: list[str] = []) -> list[str]:
    items.append(item)
    return items

first = add_item("a")
second = add_item("b")
mine = ["x"]
add_item("y", mine)

Predict what each variable holds after the program finishes. The program is not run for you. Change the values below to your predictions:

  • final_first is the value of first at the end
  • final_second is the value of second at the end
  • final_mine is the value of mine at the end
Hint 1

The default list is created once, when the function is defined. Both calls without a list use it, so first and second are the same list holding both items. The call with mine uses mine instead.

Resources: Types: union

Quizzes

  1. Does the following program contradict the idea that tuples are immutable? Why or why not?
def tuple_madness(tup):
    return tup[1:]
No, it does not. `tuple_madness` returns a brand new tuple that contains the same items as `tup` except the first. The original `tup` is never modified, so immutability still holds.
  1. Write a function named insert_front that takes as a parameter a list and a value to add at the front of the list. It should not return anything; it should mutate the original list. For example, if the arguments passed to the function are [9, -55, 37] and "bob", then after calling the function, the list should now be ["bob", 9, -55, 37].
Hint 1

You don't need to return anything. Assign a one-element list to the empty slice a_list[0:0], or use a_list.insert(0, new_element).

  1. Write a function named delete_last that takes as a parameter a list and removes the last element from that list. It should not return anything; it should mutate the original list. For example, if the list passed to the function is [7, "joe", "apple", 9.81, False], then after calling the function, the list should be [7, "joe", "apple", 9.81].
Hint 1

Use the del operator with the index -1, which refers to the last element.

  1. Predict the result. The function changes the list in two different ways.
def change(values: list[int]) -> None:
    values.append(4)
    values = [9, 9]
    values.append(5)

nums = [1, 2, 3]
change(nums)

Predict what each variable holds after the program finishes. The program is not run for you. Change the values below to your predictions:

  • final_nums is the value of nums at the end
Hint 1

values.append(4) changes the list that nums refers to. Then values = [9, 9] makes values refer to a new list, so the last append changes that new list and not nums.