Lists, Tuples and Slicing
38 min · Intermediate
Need help? Ask a tutor on WhatsApp (opens in a new tab)Almost every Python interview starts with lists and tuples. The interviewer wants more than 'one changes, one does not'. They want to hear how a list is stored, what each operation costs and what a slice really copies.
List vs tuple
A tuple is an ordered, fixed-size sequence. Its items cannot be added, removed or replaced, but a mutable item inside it can still change.
A list can change after it is made. A tuple cannot. The program tries to change a tuple, then changes a list that sits inside a tuple.
t = ('Asha', 'Ravi')try: t[0] = 'Meena'except TypeError: print('tuple cannot change')box = (['A-1'], 5)box[0].append('A-2')print(box)Example 1 · A tuple freezes membership, not contents
box = (['A-1'], 5)box[0].append('A-2')print(box)try: box[1] = 6except TypeError as err: print(type(err).__name__)try: hash(box)except TypeError as err: print(err)(['A-1', 'A-2'], 5) TypeError unhashable type: 'list'
What this code does
- The list inside the tuple grows, so the printed tuple changes.
- Assigning to a tuple item fails with TypeError.
- hash fails because a list inside is unhashable, so box cannot be a dictionary key.
A tuple fixes which objects it holds, not what those objects contain. The list inside box is still a list, so it can grow. A tuple is hashable only when every item inside is hashable, so box cannot be a dictionary key.
Use a tuple for a fixed record such as (latitude, longitude) and a list for a collection that grows or shrinks.
How a list is stored
A dynamic array is a block of memory that holds items side by side and grows when full. Python lists store references to objects in such a block.
A list is a dynamic array of references. Appending is cheap because spare room is kept at the end. Adding at the front shifts every item, so it is slow.
orders = ['A-1', 'A-2', 'A-3']orders.append('A-4')orders.insert(0, 'A-0')last = orders.pop()first = orders.pop(0)print(orders, last, first)Example 2 · The repeated inner list trap
shared = [[0] * 2] * 2shared[0][0] = 1print(shared)separate = [[0] * 2 for _ in range(2)]separate[0][0] = 1print(separate)print(shared[0] is shared[1])[[1, 0], [1, 0]] [[1, 0], [0, 0]] True
What this code does
- Multiplying a list repeats the same inner list object, not copies.
- A comprehension builds a new inner list on each pass.
- The final line shows both rows are one object.
append and pop() at the end take O(1) amortized time: the list over-allocates, so most appends need no copying. insert(0, x), pop(0) and del a[i] move all later items, so they take O(n). x in a scans the list, also O(n), while a[i] is O(1).
Because the list holds references, [[0] * 3] * 2 repeats one inner list twice.
Slicing and what it costs
A shallow copy is a new container that holds the same objects as the original. Changing a nested mutable object shows through both containers.
A slice a[start:stop:step] builds a new list. It costs time and memory in proportion to its length. The new list is a shallow copy: it holds the same objects.
items = [10, 20, 30, 40, 50]print(items[1:4], items[-2:], items[::-1])print(items[3:99], items[7:])copy = items[:]print(copy == items, copy is items)Example 3 · Slices clip and copy
a = list(range(10))print(a[2:5], a[-3:], a[::2])print(a[8:100], a[20:])b = a[:]print(b == a, b is a)a[2:5] = []print(a)[2, 3, 4] [7, 8, 9] [0, 2, 4, 6, 8] [8, 9] [] True False [0, 1, 5, 6, 7, 8, 9]
What this code does
- Slices with out-of-range ends return what exists, or an empty list.
- a[:] makes an equal copy that is a different object.
- Assigning an empty list to a slice deletes those items.
Example 4 · A slice is only a shallow copy
orders = [['A-1', 10], ['A-2', 20]]snapshot = orders[:]snapshot[0][1] = 99snapshot.append(['A-3', 30])print(orders)print(len(orders), len(snapshot))[['A-1', 99], ['A-2', 20]] 2 3
What this code does
- The inner list is shared, so the change shows in orders.
- The append changes only the new outer list.
A slice never raises IndexError: out-of-range ends are clipped. a[:] is a full copy in O(n). Slicing inside a loop or recursion, such as f(a[1:]), copies on every step and turns an O(n) job into O(n squared).
Shallow means inner objects are shared. Use copy.deepcopy when nested lists must be independent.
Next step
Check your understanding
12 quick questions