Python lists, tuples and sets interview questions, with answers
Lists, tuples and sets are the containers almost every Python answer uses, so interviewers use them to test whether you know what happens underneath: which ones can change, what copying really copies, what a slice returns, and what each operation costs. Most of the questions arrive as a few lines of code and 'what does this print?'. Every answer below comes with code you can run, and every output shown was produced by running it.
The questions go from choosing a container to sorting, copying and slicing, and end with sets and the costs behind them. Then take the free Python diagnostic — ten questions across every Python topic in the bank — to see which of these you can explain but not yet predict.
The questions, with answers
1.What is the difference between a list and a tuple?
In short: A list is mutable and a tuple is not; a tuple of hashable items is itself hashable, so it can be a dict key or set member, which a list never can.
Beyond mutability, the difference is intent: a list holds a variable number of similar items that you add to and sort, while a tuple is a fixed record, such as a coordinate or a row, and unpacking it is idiomatic. Because a tuple cannot change, Python can hash it, and that is where a tuple is required rather than merely preferred: a set of visited positions needs tuple elements, as below. The rule is recursive, so a tuple that contains a list is not hashable. Tuples are also slightly smaller and faster to create, but that is rarely the reason to choose one.
seen = {(2, 5)} print((2, 5) in seen) # True seen.add([2, 5]) # TypeError2.How do you sort by a custom key, and is Python's sort stable?
In short: Pass key=, a function computed once per item, to sort() or sorted(); both are stable, so items with equal keys keep their original order, which makes sorting by several keys straightforward.
sort() sorts a list in place and returns None, while sorted() accepts any iterable and returns a new list; the sample question on this page turns on exactly that difference. Both take key=, a function applied once to each item, and reverse=True. Python's sort is stable: items with equal keys keep the order they had, so below 'a' stays before 'd' and 'bb' before 'cc'. Stability is what makes multi-key sorting work: sort by the secondary key first, then by the primary key. For a single combined order, a key that returns a tuple, such as key=lambda s: (len(s), s), does it in one pass.
w = ["bb", "a", "cc", "d"] w.sort(key=len) print(w) # ['a', 'd', 'bb', 'cc']
3.What does copying a list actually copy?
In short: A slice, list() or .copy() makes a shallow copy: a new outer list whose items are the same objects, so nested lists are still shared; copy.deepcopy copies all the way down.
After b = a[:], a and b are different lists, so rebinding b[1] leaves a alone, but b[0] and a[0] are the same inner list, so changing b[0][0] changes a too, as below. That is the whole meaning of shallow: only one level is copied. copy.deepcopy copies every mutable object it reaches, recursively, at a higher cost. The same sharing explains why multiplying a list of lists repeats one inner list rather than creating new ones; build nested lists with a comprehension, which creates a fresh inner list on every pass.
a = [[0, 0], [0, 0]] b = a[:] b[0][0] = 1 b[1] = [5, 5] print(a) # [[1, 0], [0, 0]]
4.How does slicing work, and what does slice assignment do?
In short: s[start:stop:step] returns a new list from start up to but not including stop; assigning to a slice replaces that span in place and can change the list's length.
Slices use the same half-open rule as range, accept negative indices counted from the end, and never raise IndexError: an out-of-range slice is simply shorter or empty. A step picks every nth item, and a negative step walks backwards, so s[::-1] is a reversed copy. Reading a slice copies; writing to one does not. Below, s[1:3] = ['a'] replaces two items with one, shrinking the list, and s[:] = other replaces the contents while keeping the same list object, which matters when other names refer to it. del s[::2] removes every other item.
s = [0, 1, 2, 3, 4, 5] print(s[1:4], s[::2]) # [1, 2, 3] [0, 2, 4] s[1:3] = ["a"] print(s) # [0, 'a', 3, 4, 5]
5.What is the difference between append, extend and +?
In short: append adds its argument as one item, extend adds each item of an iterable, and + builds a new list, while += extends the existing list in place.
xs.append([4, 5]) makes the list one item longer, with a list inside it; xs.extend([4, 5]) makes it two items longer. Both change the list in place and return None. The + operator builds and returns a new list and leaves both operands untouched, while xs += ys extends xs itself, which differs from xs = xs + ys when another name refers to the same list: += changes the object both names see, as below. extend and += accept any iterable, so xs += 'ab' adds two one-character strings, while xs + 'ab' raises TypeError.
xs = [1, 2] ys = xs xs += [3] print(ys) # [1, 2, 3] xs = xs + [4] print(ys) # [1, 2, 3]
6.What are sets used for, and which operations do they support?
In short: A set holds unique hashable items with average O(1) membership tests; it supports union |, intersection &, difference - and symmetric difference ^, and {} is an empty dict, not a set.
The two everyday uses are removing duplicates, set(xs), and fast membership, x in s, which looks in a hash table instead of scanning the way a list does. The operators below combine sets mathematically, and each has a method form, such as a.union(b), that accepts any iterable. Sets are unordered: never rely on the order they print in, and use dict.fromkeys(xs) when you need unique items in their original order. Items must be hashable, so a set of lists fails, and the empty literal {} is a dict — write set() for an empty set. frozenset is the immutable, hashable version.
a = {1, 2, 3} b = {2, 3, 4} print(a & b, a | b) # {2, 3} {1, 2, 3, 4} print(a - b, a ^ b) # {1} {1, 4}7.What is the time complexity of common list and set operations?
In short: Appending and popping at a list's end are O(1) amortised, while inserting or popping at the front and x in a list are O(n); x in a set is O(1) on average, and collections.deque makes a proper queue.
A list is a dynamic array: indexing and appending at the end are fast, but inserting or removing at the front shifts every other item, so a list used as a queue costs O(n) per operation. collections.deque is a double-ended queue with O(1) appends and pops at both ends, the right structure for a breadth-first search, as below. Membership is the other trap: x in some_list scans the list, so testing many values against a large list is O(n) each time, while converting it to a set once makes each test O(1) on average. Sorting is O(n log n), and len() is O(1) for all three types.
from collections import deque q = deque([1, 2, 3]) q.appendleft(0) q.pop() print(q) # deque([0, 1, 2])
8.How does tuple unpacking work, including a starred target?
In short: Assigning to a comma-separated list of names unpacks any iterable item by item, a single *name collects the leftovers as a list, and a, b = b, a swaps two values.
The right-hand side is evaluated completely first and then unpacked into the targets from left to right, which is why a, b = b, a swaps without a temporary variable. The counts must match unless one target is starred: first, *rest = xs takes the first item and puts the remainder in a list, which may be empty, while too few or too many items raise ValueError. Unpacking nests, so for i, (name, score) in enumerate(pairs) unpacks inside a loop header. The same * syntax spreads iterables inside list and tuple displays: [*a, *b] joins two lists.
first, *rest = [1, 2, 3] print(first, rest) # 1 [2, 3] a, b = 1, 2 a, b = b, a print(a, b) # 2 1
How the diagnostic asks it
One question from the Python bank, exactly as a sitting would show it. The bank has 4 on lists, tuples & sets and 34 across Python.
What does this code print?
nums = [3, 1, 2] result = nums.sort() print(result, nums)
- 1[1, 2, 3] [1, 2, 3]
- 2None [1, 2, 3]correct
- 3[1, 2, 3] [3, 1, 2]
- 4None [3, 1, 2]
list.sort() rearranges the list in place and returns None, so result is None and nums is [1, 2, 3]. [1, 2, 3] [1, 2, 3] assumes sort() also returns the list, [1, 2, 3] [3, 1, 2] describes sorted(nums), which returns a new list and leaves nums alone, and None [3, 1, 2] assumes nothing was sorted.
Measure it
Reading answers tells you what’s true. A diagnostic tells you what you get wrong.
10 Python questions across its topics, easy to hard, about fifteen minutes. You get a readiness figure with the arithmetic shown, the topics you missed named, and a practice set sized for today. Free: 1 diagnostic a month and 15 problems a day. No card.