Lists
Build, index, slice, and grow Python's ordered, mutable collection.
Why lists are the default container
Reach for a list any time you have an ordered sequence you will read, grow, or reshape: rows streamed from a query, tokens parsed from a line, a batch of records waiting to be written. It is the collection you build up in a loop and hand off to the next stage of a pipeline. Because it is both ordered and changeable, one list can serve as your accumulator, your buffer, and your result all at once.
The mental model: a dynamic array of references
A Python list is a dynamic array. Under the hood it holds a contiguous block of slots pointing at your objects, and the interpreter resizes that block for you as the list grows. Two consequences follow. First, reaching any position by index is a direct jump, so nums[i] costs the same whether the list has 3 items or 3 million. Second, the list stores references, not copies, so the same object can sit in more than one list at once.
You write a list with square brackets and index it like a string, starting at 0:
nums = [10, 20, 30]
nums[0] # 10 first item
nums[-1] # 30 negative counts from the end
nums[1:] # [20, 30] a slice returns a new list
len(nums) # 3 how many items
Mutability: changing in place
Unlike strings and tuples, lists are mutable. The methods below change the existing list rather than returning a new one:
nums.append(40) # [10, 20, 30, 40] add to the end
nums.insert(0, 5) # [5, 10, 20, 30, 40] add at an index
nums.remove(20) # [5, 10, 30, 40] remove the first 20
The demo below starts from [10, 20, 30], calls append(40), and prints [10, 20, 30, 40], so nums[-1] is 40 and len(nums) is 4. Notice append returns None: it mutates the list and hands nothing back, which is exactly why the Apply task asks you to append and then return items on a separate step.
That split, mutate here and return there, applies to every list operation:
| You call | Effect on the original list | What it hands back |
|---|---|---|
| lst.append(x) | x is added to the end | None |
| lst.sort() | lst is reordered in place | None |
| lst.reverse() | lst is reversed in place | None |
| lst.pop() | the last item is removed | the removed item |
| sorted(lst) | untouched | a new sorted list |
| reversed(lst) | untouched | a lazy iterator |
| lst + [x] | untouched | a new list |
Pitfall: aliasing shares one object
Assignment copies the reference, not the list. Both names then point at the same object:
a = [1, 2, 3]
b = a
b.append(4)
print(a) # [1, 2, 3, 4] a changed too
If you wanted an independent copy, make one explicitly with a[:], list(a), or a.copy(). Interns lose hours to a helper that quietly mutates the caller's list.
For the Practice task, the middle index is len(items) // 2. Integer division // floors the result, so a 5-item list gives index 2, landing on the true center 30. On an even-length list it picks the right-of-center item, which is the intended, deterministic rule.
Interview nuance: know the cost of each operation. Indexing and append are effectively O(1) (append is amortized O(1) because the backing array over-allocates), but insert(0, x), remove, and pop(0) are O(n) because every later element shifts one slot. If a problem needs fast inserts or removes at the front, that is the signal to reach for collections.deque instead of a list.
nums = [10, 20, 30]
nums.append(40)
print(nums) # [10, 20, 30, 40]
print(nums[-1]) # 40
print(len(nums)) # 4Apply
Your turn
The task this lesson builds to.
Implement add_item(items, value): append value to the list items and return the list.
For ([1, 2], 3) return [1, 2, 3].
3 hints and 4 automated checks are waiting in the workspace.
Practice
Make it stick
A second problem on the same idea, so it survives past today.
Implement middle_item(items): return the item at the middle index of the list.
The middle index is len(items) // 2. For [10, 20, 30, 40, 50] return 30.
2 hints and 4 automated checks are waiting in the workspace.