Comprehensions, Safe Iteration and Sorting
38 min · Intermediate
Need help? Ask a tutor on WhatsApp (opens in a new tab)These three topics fill many interview rounds: writing a comprehension in one line, spotting the bug in a loop that changes its own list, and sorting records by more than one field. Each one has a classic trap.
Comprehensions
A comprehension is an expression that builds a list, dict or set from a loop, with an optional filter. It reads as: the value, for each item, if the condition holds.
A comprehension builds a collection from a loop in one expression. The condition at the end filters; a conditional expression at the front chooses a value.
marks = [72, -1, 95, 60]valid = [m for m in marks if m >= 0]fixed = [m if m >= 0 else 0 for m in marks]by_mark = {m: m >= 70 for m in valid}total = sum(m for m in valid)print(valid, fixed, by_mark, total)Example 1 · Four comprehension forms
nums = [1, 2, 3, 4, 5, 6]print([n * n for n in nums if n % 2 == 0])print({n: n * n for n in nums[:3]})print({n % 3 for n in nums})gen = (n * n for n in nums)print(sum(gen))print(type(gen).__name__)[4, 16, 36]
{1: 1, 2: 4, 3: 9}
{0, 1, 2}
91
generatorWhat this code does
- The list keeps squares of even numbers.
- The dict maps numbers to squares.
- The set holds each remainder once.
- The generator is consumed by sum without building a list.
Example 2 · Filter at the end, choice at the front
scores = [40, -5, 70, -1, 90]print([s for s in scores if s >= 0])print([s if s >= 0 else 0 for s in scores])grid = [[1, 2], [3, 4]]print([x for row in grid for x in row])print([list(col) for col in zip(*grid)])[40, 70, 90] [40, 0, 70, 0, 90] [1, 2, 3, 4] [[1, 3], [2, 4]]
What this code does
- if at the end drops items; if/else at the front keeps every item.
- In nested loops, the outer loop comes first.
- zip(*grid) turns rows into columns.
[... for ...] makes a list, {k: v for ...} a dict, {x for ...} a set. Round brackets make a generator, which produces one item at a time and uses almost no memory. A comprehension has its own scope, so its loop variable does not leak into the surrounding code.
Changing a collection while looping
Iterator invalidation is the bug that appears when a collection changes size during a loop. A list loop may skip items; a dict or set loop raises RuntimeError.
A list loop keeps a position number. Removing an item shifts the later items left, so the loop jumps over one. A dict or set refuses the change and raises an error.
queue = [1, 2, 2, 3]for n in queue: if n == 2: queue.remove(n)print(queue)queue = [1, 2, 2, 3]queue = [n for n in queue if n != 2]print(queue)Example 3 · Removing during a loop skips an item
queue = [1, 2, 2, 3]for n in queue: if n == 2: queue.remove(n)print(queue)queue = [1, 2, 2, 3]queue[:] = [n for n in queue if n != 2]print(queue)[1, 2, 3] [1, 3]
What this code does
- After the first remove, the second 2 slides into the visited position and is skipped.
- Rebuilding the list in place removes every 2.
Example 4 · Dict changes during a loop
stock = {'pen': 0, 'book': 4, 'cup': 0}try: for item in stock: if stock[item] == 0: del stock[item]except RuntimeError as err: print(err)for item in list(stock): if stock[item] == 0: del stock[item]print(stock)dictionary changed size during iteration
{'book': 4}What this code does
- Deleting a key during a loop over the dict raises RuntimeError.
- Looping over list(stock) iterates a snapshot, so deleting is safe.
The first loop prints [1, 2, 3]: one 2 is left behind. Safe patterns are to build a new collection, to loop over a copy such as list(d) or queue[:], or to assign back with queue[:] = new_list. Changing the value of an existing dict key during a loop is allowed; adding or deleting keys is not.
Sorting and key
A stable sort keeps items with equal keys in the same relative order as before. Python sorted and list.sort are both stable.
sorted(x) returns a new list; x.sort() changes the list and returns None. The key function turns each item into the value to compare. Sorting is stable: equal keys keep their original order.
rows = [('Ravi', 88), ('Asha', 95), ('Meena', 88)]by_name = sorted(rows)by_mark = sorted(rows, key=lambda r: r[1], reverse=True)by_both = sorted(rows, key=lambda r: (-r[1], r[0]))print(by_mark)print(by_both)Example 5 · key, reverse and stability
words = ['pear', 'fig', 'apple', 'kiwi', 'plum']print(sorted(words, key=len))print(sorted(words, key=len, reverse=True))print(list(reversed(sorted(words, key=len))))rows = [('Ravi', 88), ('Asha', 95), ('Meena', 88)]print(sorted(rows, key=lambda r: (-r[1], r[0])))['fig', 'pear', 'kiwi', 'plum', 'apple']
['apple', 'pear', 'kiwi', 'plum', 'fig']
['apple', 'plum', 'kiwi', 'pear', 'fig']
[('Asha', 95), ('Meena', 88), ('Ravi', 88)]What this code does
- Equal lengths keep their original order.
- reverse=True keeps equal items in original order; reversed(sorted()) flips them.
- The tuple key sorts by mark descending, then name.
Python uses Timsort: O(n log n) in the worst case, close to O(n) on data that is already nearly sorted, and O(n) extra space. key runs once per item, not once per comparison. For mixed directions, negate numbers inside a tuple key, or sort twice and rely on stability, least important field first.
Next step
Check your understanding
12 quick questions